@croacroa/react-native-template 1.0.0 → 2.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 (69) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +13 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +32 -2
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +229 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/withAccessibility.tsx +272 -0
@@ -1,256 +1,256 @@
1
- import { useEffect, useCallback } from "react";
2
- import * as Linking from "expo-linking";
3
- import { router } from "expo-router";
4
-
5
- // Define your app's deep link routes
6
- type DeepLinkRoute =
7
- | { path: "login"; params?: never }
8
- | { path: "register"; params?: never }
9
- | { path: "reset-password"; params: { token: string } }
10
- | { path: "profile"; params?: { userId?: string } }
11
- | { path: "settings"; params?: never }
12
- | { path: "post"; params: { postId: string } };
13
-
14
- interface ParsedDeepLink {
15
- route: DeepLinkRoute | null;
16
- rawUrl: string;
17
- }
18
-
19
- /**
20
- * Parse a deep link URL into a route and params
21
- */
22
- function parseDeepLink(url: string): ParsedDeepLink {
23
- try {
24
- const parsed = Linking.parse(url);
25
- const { path, queryParams } = parsed;
26
-
27
- if (!path) {
28
- return { route: null, rawUrl: url };
29
- }
30
-
31
- // Map URL paths to app routes
32
- switch (path) {
33
- case "login":
34
- return { route: { path: "login" }, rawUrl: url };
35
-
36
- case "register":
37
- case "signup":
38
- return { route: { path: "register" }, rawUrl: url };
39
-
40
- case "reset-password":
41
- if (queryParams?.token && typeof queryParams.token === "string") {
42
- return {
43
- route: {
44
- path: "reset-password",
45
- params: { token: queryParams.token },
46
- },
47
- rawUrl: url,
48
- };
49
- }
50
- return { route: null, rawUrl: url };
51
-
52
- case "profile":
53
- return {
54
- route: {
55
- path: "profile",
56
- params: queryParams?.userId
57
- ? { userId: String(queryParams.userId) }
58
- : undefined,
59
- },
60
- rawUrl: url,
61
- };
62
-
63
- case "settings":
64
- return { route: { path: "settings" }, rawUrl: url };
65
-
66
- case "post":
67
- if (queryParams?.id && typeof queryParams.id === "string") {
68
- return {
69
- route: { path: "post", params: { postId: queryParams.id } },
70
- rawUrl: url,
71
- };
72
- }
73
- return { route: null, rawUrl: url };
74
-
75
- default: {
76
- // Try to handle paths like /post/123
77
- const pathParts = path.split("/");
78
- if (pathParts[0] === "post" && pathParts[1]) {
79
- return {
80
- route: { path: "post", params: { postId: pathParts[1] } },
81
- rawUrl: url,
82
- };
83
- }
84
- if (pathParts[0] === "profile" && pathParts[1]) {
85
- return {
86
- route: { path: "profile", params: { userId: pathParts[1] } },
87
- rawUrl: url,
88
- };
89
- }
90
- return { route: null, rawUrl: url };
91
- }
92
- }
93
- } catch (error) {
94
- console.error("Failed to parse deep link:", error);
95
- return { route: null, rawUrl: url };
96
- }
97
- }
98
-
99
- /**
100
- * Navigate to a deep link route
101
- */
102
- function navigateToRoute(route: DeepLinkRoute): void {
103
- switch (route.path) {
104
- case "login":
105
- router.replace("/(public)/login");
106
- break;
107
-
108
- case "register":
109
- router.replace("/(public)/register");
110
- break;
111
-
112
- case "reset-password":
113
- // Navigate to forgot password with token
114
- router.push({
115
- pathname: "/(public)/forgot-password",
116
- params: { token: route.params.token },
117
- });
118
- break;
119
-
120
- case "profile":
121
- if (route.params?.userId) {
122
- // Navigate to specific user profile
123
- router.push({
124
- pathname: "/(auth)/profile",
125
- params: { userId: route.params.userId },
126
- });
127
- } else {
128
- // Navigate to own profile
129
- router.push("/(auth)/profile");
130
- }
131
- break;
132
-
133
- case "settings":
134
- router.push("/(auth)/settings");
135
- break;
136
-
137
- case "post":
138
- // You'll need to create this route
139
- router.push({
140
- pathname: "/(auth)/post/[id]" as const,
141
- params: { id: route.params.postId },
142
- } as Parameters<typeof router.push>[0]);
143
- break;
144
- }
145
- }
146
-
147
- interface UseDeepLinkingOptions {
148
- /**
149
- * Called when a deep link is received but couldn't be parsed
150
- */
151
- onUnknownLink?: (url: string) => void;
152
-
153
- /**
154
- * Called before navigating to allow custom handling
155
- * Return false to prevent default navigation
156
- */
157
- onBeforeNavigate?: (route: DeepLinkRoute) => boolean | void;
158
-
159
- /**
160
- * Whether deep linking is enabled
161
- * @default true
162
- */
163
- enabled?: boolean;
164
- }
165
-
166
- /**
167
- * Hook to handle deep links in your app
168
- *
169
- * @example
170
- * ```tsx
171
- * function App() {
172
- * useDeepLinking({
173
- * onUnknownLink: (url) => console.log('Unknown link:', url),
174
- * onBeforeNavigate: (route) => {
175
- * // Custom validation before navigating
176
- * if (route.path === 'profile' && !isAuthenticated) {
177
- * router.push('/login');
178
- * return false; // Prevent default navigation
179
- * }
180
- * },
181
- * });
182
- *
183
- * return <App />;
184
- * }
185
- * ```
186
- */
187
- export function useDeepLinking(options: UseDeepLinkingOptions = {}): void {
188
- const { onUnknownLink, onBeforeNavigate, enabled = true } = options;
189
-
190
- const handleDeepLink = useCallback(
191
- (event: { url: string }) => {
192
- if (!enabled) return;
193
-
194
- const { route, rawUrl } = parseDeepLink(event.url);
195
-
196
- if (!route) {
197
- onUnknownLink?.(rawUrl);
198
- console.log("Unknown deep link:", rawUrl);
199
- return;
200
- }
201
-
202
- // Allow custom handling before navigation
203
- const shouldNavigate = onBeforeNavigate?.(route);
204
- if (shouldNavigate === false) {
205
- return;
206
- }
207
-
208
- navigateToRoute(route);
209
- },
210
- [enabled, onUnknownLink, onBeforeNavigate]
211
- );
212
-
213
- useEffect(() => {
214
- if (!enabled) return;
215
-
216
- // Handle deep links when app is already open
217
- const subscription = Linking.addEventListener("url", handleDeepLink);
218
-
219
- // Handle deep link that opened the app
220
- Linking.getInitialURL().then((url) => {
221
- if (url) {
222
- handleDeepLink({ url });
223
- }
224
- });
225
-
226
- return () => {
227
- subscription.remove();
228
- };
229
- }, [enabled, handleDeepLink]);
230
- }
231
-
232
- /**
233
- * Get the app's deep link URL prefix
234
- */
235
- export function getDeepLinkPrefix(): string {
236
- return Linking.createURL("/");
237
- }
238
-
239
- /**
240
- * Create a deep link URL for the app
241
- *
242
- * @example
243
- * ```ts
244
- * const url = createDeepLink('profile', { userId: '123' });
245
- * // Returns: yourapp://profile?userId=123
246
- * ```
247
- */
248
- export function createDeepLink(
249
- path: string,
250
- params?: Record<string, string>
251
- ): string {
252
- return Linking.createURL(path, { queryParams: params });
253
- }
254
-
255
- export { parseDeepLink, navigateToRoute };
256
- export type { DeepLinkRoute, ParsedDeepLink };
1
+ import { useEffect, useCallback } from "react";
2
+ import * as Linking from "expo-linking";
3
+ import { router } from "expo-router";
4
+
5
+ // Define your app's deep link routes
6
+ type DeepLinkRoute =
7
+ | { path: "login"; params?: never }
8
+ | { path: "register"; params?: never }
9
+ | { path: "reset-password"; params: { token: string } }
10
+ | { path: "profile"; params?: { userId?: string } }
11
+ | { path: "settings"; params?: never }
12
+ | { path: "post"; params: { postId: string } };
13
+
14
+ interface ParsedDeepLink {
15
+ route: DeepLinkRoute | null;
16
+ rawUrl: string;
17
+ }
18
+
19
+ /**
20
+ * Parse a deep link URL into a route and params
21
+ */
22
+ function parseDeepLink(url: string): ParsedDeepLink {
23
+ try {
24
+ const parsed = Linking.parse(url);
25
+ const { path, queryParams } = parsed;
26
+
27
+ if (!path) {
28
+ return { route: null, rawUrl: url };
29
+ }
30
+
31
+ // Map URL paths to app routes
32
+ switch (path) {
33
+ case "login":
34
+ return { route: { path: "login" }, rawUrl: url };
35
+
36
+ case "register":
37
+ case "signup":
38
+ return { route: { path: "register" }, rawUrl: url };
39
+
40
+ case "reset-password":
41
+ if (queryParams?.token && typeof queryParams.token === "string") {
42
+ return {
43
+ route: {
44
+ path: "reset-password",
45
+ params: { token: queryParams.token },
46
+ },
47
+ rawUrl: url,
48
+ };
49
+ }
50
+ return { route: null, rawUrl: url };
51
+
52
+ case "profile":
53
+ return {
54
+ route: {
55
+ path: "profile",
56
+ params: queryParams?.userId
57
+ ? { userId: String(queryParams.userId) }
58
+ : undefined,
59
+ },
60
+ rawUrl: url,
61
+ };
62
+
63
+ case "settings":
64
+ return { route: { path: "settings" }, rawUrl: url };
65
+
66
+ case "post":
67
+ if (queryParams?.id && typeof queryParams.id === "string") {
68
+ return {
69
+ route: { path: "post", params: { postId: queryParams.id } },
70
+ rawUrl: url,
71
+ };
72
+ }
73
+ return { route: null, rawUrl: url };
74
+
75
+ default: {
76
+ // Try to handle paths like /post/123
77
+ const pathParts = path.split("/");
78
+ if (pathParts[0] === "post" && pathParts[1]) {
79
+ return {
80
+ route: { path: "post", params: { postId: pathParts[1] } },
81
+ rawUrl: url,
82
+ };
83
+ }
84
+ if (pathParts[0] === "profile" && pathParts[1]) {
85
+ return {
86
+ route: { path: "profile", params: { userId: pathParts[1] } },
87
+ rawUrl: url,
88
+ };
89
+ }
90
+ return { route: null, rawUrl: url };
91
+ }
92
+ }
93
+ } catch (error) {
94
+ console.error("Failed to parse deep link:", error);
95
+ return { route: null, rawUrl: url };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Navigate to a deep link route
101
+ */
102
+ function navigateToRoute(route: DeepLinkRoute): void {
103
+ switch (route.path) {
104
+ case "login":
105
+ router.replace("/(public)/login");
106
+ break;
107
+
108
+ case "register":
109
+ router.replace("/(public)/register");
110
+ break;
111
+
112
+ case "reset-password":
113
+ // Navigate to forgot password with token
114
+ router.push({
115
+ pathname: "/(public)/forgot-password",
116
+ params: { token: route.params.token },
117
+ });
118
+ break;
119
+
120
+ case "profile":
121
+ if (route.params?.userId) {
122
+ // Navigate to specific user profile
123
+ router.push({
124
+ pathname: "/(auth)/profile",
125
+ params: { userId: route.params.userId },
126
+ });
127
+ } else {
128
+ // Navigate to own profile
129
+ router.push("/(auth)/profile");
130
+ }
131
+ break;
132
+
133
+ case "settings":
134
+ router.push("/(auth)/settings");
135
+ break;
136
+
137
+ case "post":
138
+ // You'll need to create this route
139
+ router.push({
140
+ pathname: "/(auth)/post/[id]" as const,
141
+ params: { id: route.params.postId },
142
+ } as Parameters<typeof router.push>[0]);
143
+ break;
144
+ }
145
+ }
146
+
147
+ interface UseDeepLinkingOptions {
148
+ /**
149
+ * Called when a deep link is received but couldn't be parsed
150
+ */
151
+ onUnknownLink?: (url: string) => void;
152
+
153
+ /**
154
+ * Called before navigating to allow custom handling
155
+ * Return false to prevent default navigation
156
+ */
157
+ onBeforeNavigate?: (route: DeepLinkRoute) => boolean | void;
158
+
159
+ /**
160
+ * Whether deep linking is enabled
161
+ * @default true
162
+ */
163
+ enabled?: boolean;
164
+ }
165
+
166
+ /**
167
+ * Hook to handle deep links in your app
168
+ *
169
+ * @example
170
+ * ```tsx
171
+ * function App() {
172
+ * useDeepLinking({
173
+ * onUnknownLink: (url) => console.log('Unknown link:', url),
174
+ * onBeforeNavigate: (route) => {
175
+ * // Custom validation before navigating
176
+ * if (route.path === 'profile' && !isAuthenticated) {
177
+ * router.push('/login');
178
+ * return false; // Prevent default navigation
179
+ * }
180
+ * },
181
+ * });
182
+ *
183
+ * return <App />;
184
+ * }
185
+ * ```
186
+ */
187
+ export function useDeepLinking(options: UseDeepLinkingOptions = {}): void {
188
+ const { onUnknownLink, onBeforeNavigate, enabled = true } = options;
189
+
190
+ const handleDeepLink = useCallback(
191
+ (event: { url: string }) => {
192
+ if (!enabled) return;
193
+
194
+ const { route, rawUrl } = parseDeepLink(event.url);
195
+
196
+ if (!route) {
197
+ onUnknownLink?.(rawUrl);
198
+ console.log("Unknown deep link:", rawUrl);
199
+ return;
200
+ }
201
+
202
+ // Allow custom handling before navigation
203
+ const shouldNavigate = onBeforeNavigate?.(route);
204
+ if (shouldNavigate === false) {
205
+ return;
206
+ }
207
+
208
+ navigateToRoute(route);
209
+ },
210
+ [enabled, onUnknownLink, onBeforeNavigate]
211
+ );
212
+
213
+ useEffect(() => {
214
+ if (!enabled) return;
215
+
216
+ // Handle deep links when app is already open
217
+ const subscription = Linking.addEventListener("url", handleDeepLink);
218
+
219
+ // Handle deep link that opened the app
220
+ Linking.getInitialURL().then((url) => {
221
+ if (url) {
222
+ handleDeepLink({ url });
223
+ }
224
+ });
225
+
226
+ return () => {
227
+ subscription.remove();
228
+ };
229
+ }, [enabled, handleDeepLink]);
230
+ }
231
+
232
+ /**
233
+ * Get the app's deep link URL prefix
234
+ */
235
+ export function getDeepLinkPrefix(): string {
236
+ return Linking.createURL("/");
237
+ }
238
+
239
+ /**
240
+ * Create a deep link URL for the app
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * const url = createDeepLink('profile', { userId: '123' });
245
+ * // Returns: yourapp://profile?userId=123
246
+ * ```
247
+ */
248
+ export function createDeepLink(
249
+ path: string,
250
+ params?: Record<string, string>
251
+ ): string {
252
+ return Linking.createURL(path, { queryParams: params });
253
+ }
254
+
255
+ export { parseDeepLink, navigateToRoute };
256
+ export type { DeepLinkRoute, ParsedDeepLink };