@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,111 @@
1
+ /**
2
+ * @fileoverview Channel subscription hook
3
+ * Subscribes to a specific WebSocket channel and accumulates messages in state.
4
+ * @module hooks/useChannel
5
+ */
6
+
7
+ import { useEffect, useState, useCallback, useRef } from "react";
8
+
9
+ import { WebSocketManager } from "@/services/realtime/websocket-manager";
10
+ import type { WebSocketMessage } from "@/services/realtime/types";
11
+
12
+ /**
13
+ * Options for the useChannel hook.
14
+ */
15
+ export interface UseChannelOptions {
16
+ /** Maximum number of messages to keep in state (default: 500) */
17
+ maxMessages?: number;
18
+ }
19
+
20
+ /**
21
+ * Return type for the useChannel hook.
22
+ *
23
+ * @typeParam T - The shape of the message payload
24
+ */
25
+ export interface UseChannelReturn<T = unknown> {
26
+ /** All messages received on this channel (oldest first) */
27
+ messages: WebSocketMessage<T>[];
28
+ /** The most recently received message, or null if none */
29
+ lastMessage: WebSocketMessage<T> | null;
30
+ /** Send a message to this channel */
31
+ send: (type: string, payload: T) => void;
32
+ }
33
+
34
+ /**
35
+ * Hook for subscribing to a WebSocket channel.
36
+ *
37
+ * Subscribes on mount, unsubscribes on unmount, and accumulates all received
38
+ * messages in a state array. Provides a convenience `send` function that
39
+ * automatically targets the subscribed channel.
40
+ *
41
+ * @typeParam T - The shape of the message payload
42
+ * @param manager - The WebSocketManager instance (from useWebSocket)
43
+ * @param channel - The channel name to subscribe to
44
+ * @returns Object with messages array, lastMessage, and channel-scoped send
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * function ChatRoom({ roomId }: { roomId: string }) {
49
+ * const { manager } = useWebSocket({ url: WS_URL });
50
+ * const { messages, lastMessage, send } = useChannel<ChatPayload>(
51
+ * manager,
52
+ * `chat:${roomId}`
53
+ * );
54
+ *
55
+ * const handleSend = (text: string) => {
56
+ * send('chat:message', { text, sender: currentUser.id });
57
+ * };
58
+ *
59
+ * return (
60
+ * <FlatList
61
+ * data={messages}
62
+ * renderItem={({ item }) => <MessageBubble message={item} />}
63
+ * />
64
+ * );
65
+ * }
66
+ * ```
67
+ */
68
+ export function useChannel<T = unknown>(
69
+ manager: WebSocketManager,
70
+ channel: string,
71
+ options: UseChannelOptions = {}
72
+ ): UseChannelReturn<T> {
73
+ const { maxMessages = 500 } = options;
74
+ const [messages, setMessages] = useState<WebSocketMessage<T>[]>([]);
75
+ const [lastMessage, setLastMessage] = useState<WebSocketMessage<T> | null>(
76
+ null
77
+ );
78
+ const channelRef = useRef(channel);
79
+ channelRef.current = channel;
80
+
81
+ useEffect(() => {
82
+ // Reset messages when channel changes
83
+ setMessages([]);
84
+ setLastMessage(null);
85
+
86
+ const unsubscribe = manager.subscribe<T>(channel, (message) => {
87
+ setMessages((prev) => {
88
+ const next = [...prev, message];
89
+ return next.length > maxMessages ? next.slice(-maxMessages) : next;
90
+ });
91
+ setLastMessage(message);
92
+ });
93
+
94
+ return () => {
95
+ unsubscribe();
96
+ };
97
+ }, [manager, channel]);
98
+
99
+ const send = useCallback(
100
+ (type: string, payload: T) => {
101
+ manager.send(type, payload, channelRef.current);
102
+ },
103
+ [manager]
104
+ );
105
+
106
+ return {
107
+ messages,
108
+ lastMessage,
109
+ send,
110
+ };
111
+ }
@@ -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 };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview React hook for A/B test experiment variants
3
+ * Returns the assigned variant for a given experiment, backed by the
4
+ * FeatureFlags facade.
5
+ * @module hooks/useExperiment
6
+ */
7
+
8
+ import { useState, useEffect } from "react";
9
+ import { FeatureFlags } from "@/services/feature-flags/feature-flag-adapter";
10
+
11
+ /**
12
+ * Hook that resolves the assigned variant for an A/B test experiment.
13
+ *
14
+ * @param experimentId - The experiment identifier
15
+ * @returns `{ variant, isLoading }`
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const { variant, isLoading } = useExperiment("onboarding_flow");
20
+ *
21
+ * if (isLoading) return <Loader />;
22
+ * if (variant === "variant_a") return <OnboardingA />;
23
+ * return <OnboardingControl />;
24
+ * ```
25
+ */
26
+ export function useExperiment(experimentId: string) {
27
+ const [variant, setVariant] = useState<string | null>(null);
28
+ const [isLoading, setIsLoading] = useState(true);
29
+
30
+ useEffect(() => {
31
+ setVariant(FeatureFlags.getExperimentVariant(experimentId));
32
+ setIsLoading(false);
33
+ }, [experimentId]);
34
+
35
+ return { variant, isLoading };
36
+ }