@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.
- package/.github/workflows/ci.yml +187 -184
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/CHANGELOG.md +106 -106
- package/CONTRIBUTING.md +377 -377
- package/README.md +399 -399
- package/__tests__/components/snapshots.test.tsx +131 -0
- package/__tests__/integration/auth-api.test.tsx +227 -0
- package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
- package/app/(public)/onboarding.tsx +5 -5
- package/app.config.ts +45 -2
- package/assets/images/.gitkeep +7 -7
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/SuspenseBoundary.tsx +357 -0
- package/components/providers/index.ts +13 -0
- package/components/ui/Avatar.tsx +316 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Select.tsx +240 -240
- package/components/ui/VirtualizedList.tsx +285 -0
- package/components/ui/index.ts +23 -18
- package/constants/config.ts +97 -54
- 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/hooks/index.ts +27 -25
- package/hooks/useApi.ts +102 -5
- package/hooks/useAuth.tsx +82 -0
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useMFA.ts +499 -0
- package/hooks/useNotifications.ts +39 -0
- package/hooks/useOffline.ts +32 -2
- package/hooks/usePerformance.ts +434 -434
- package/hooks/useTheme.tsx +76 -0
- package/hooks/useUpdates.ts +358 -358
- package/i18n/index.ts +194 -77
- package/i18n/locales/ar.json +101 -0
- package/i18n/locales/de.json +101 -0
- package/i18n/locales/en.json +101 -101
- package/i18n/locales/es.json +101 -0
- package/i18n/locales/fr.json +101 -101
- package/jest.config.js +4 -4
- 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 -0
- package/maestro/flows/mfa-setup.yaml +86 -0
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -0
- package/maestro/flows/offline-sync.yaml +128 -0
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +175 -170
- package/services/analytics.ts +428 -428
- package/services/api.ts +340 -340
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +626 -0
- package/services/index.ts +54 -22
- package/services/security.ts +229 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- package/utils/withAccessibility.tsx +272 -0
package/hooks/useDeepLinking.ts
CHANGED
|
@@ -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 };
|