codecruise 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# Navigation Rules - Expo Router
|
|
2
|
+
|
|
3
|
+
File-based routing patterns for Expo.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Route Structure
|
|
8
|
+
|
|
9
|
+
### NAV-001: File naming conventions
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
app/
|
|
13
|
+
├── _layout.tsx # Root layout (required)
|
|
14
|
+
├── index.tsx # Home screen (/)
|
|
15
|
+
├── settings.tsx # Settings screen (/settings)
|
|
16
|
+
├── (tabs)/ # Tab group (hidden from URL)
|
|
17
|
+
│ ├── _layout.tsx # Tab navigator
|
|
18
|
+
│ ├── home.tsx # /home
|
|
19
|
+
│ └── profile.tsx # /profile
|
|
20
|
+
├── (auth)/ # Auth group (hidden from URL)
|
|
21
|
+
│ ├── _layout.tsx # Auth stack
|
|
22
|
+
│ ├── login.tsx # /login
|
|
23
|
+
│ └── register.tsx # /register
|
|
24
|
+
├── users/
|
|
25
|
+
│ ├── index.tsx # /users
|
|
26
|
+
│ └── [id].tsx # /users/:id
|
|
27
|
+
└── +not-found.tsx # 404 screen
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### NAV-002: Layout components
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// app/_layout.tsx - Root layout
|
|
34
|
+
import { Stack } from 'expo-router';
|
|
35
|
+
import { ThemeProvider } from '@/components/theme-provider';
|
|
36
|
+
|
|
37
|
+
export default function RootLayout() {
|
|
38
|
+
return (
|
|
39
|
+
<ThemeProvider>
|
|
40
|
+
<Stack>
|
|
41
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
42
|
+
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
|
43
|
+
<Stack.Screen
|
|
44
|
+
name="modal"
|
|
45
|
+
options={{ presentation: 'modal' }}
|
|
46
|
+
/>
|
|
47
|
+
</Stack>
|
|
48
|
+
</ThemeProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// app/(tabs)/_layout.tsx - Tab layout
|
|
53
|
+
import { Tabs } from 'expo-router';
|
|
54
|
+
|
|
55
|
+
export default function TabLayout() {
|
|
56
|
+
return (
|
|
57
|
+
<Tabs>
|
|
58
|
+
<Tabs.Screen name="home" options={{ title: 'Home' }} />
|
|
59
|
+
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
|
|
60
|
+
</Tabs>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### NAV-003: Group routes logically
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Groups with () don't affect URL
|
|
69
|
+
// (auth) group -> /login, not /(auth)/login
|
|
70
|
+
|
|
71
|
+
// Shared layout for multiple screens
|
|
72
|
+
// app/(settings)/_layout.tsx
|
|
73
|
+
export default function SettingsLayout() {
|
|
74
|
+
return (
|
|
75
|
+
<Stack>
|
|
76
|
+
<Stack.Screen name="index" options={{ title: 'Settings' }} />
|
|
77
|
+
<Stack.Screen name="account" options={{ title: 'Account' }} />
|
|
78
|
+
<Stack.Screen name="privacy" options={{ title: 'Privacy' }} />
|
|
79
|
+
</Stack>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Navigation Actions
|
|
87
|
+
|
|
88
|
+
### NAV-004: Programmatic navigation
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { router } from 'expo-router';
|
|
92
|
+
|
|
93
|
+
// Push (adds to stack)
|
|
94
|
+
router.push('/users/123');
|
|
95
|
+
|
|
96
|
+
// Replace (replaces current)
|
|
97
|
+
router.replace('/home');
|
|
98
|
+
|
|
99
|
+
// Back
|
|
100
|
+
router.back();
|
|
101
|
+
|
|
102
|
+
// Dismiss modal
|
|
103
|
+
router.dismiss();
|
|
104
|
+
|
|
105
|
+
// Navigate (smart - uses push or replace)
|
|
106
|
+
router.navigate('/settings');
|
|
107
|
+
|
|
108
|
+
// Reset stack
|
|
109
|
+
router.replace('/');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### NAV-005: Type-safe navigation
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// types/routes.ts
|
|
116
|
+
export type AppRoutes = {
|
|
117
|
+
'/': undefined;
|
|
118
|
+
'/users/[id]': { id: string };
|
|
119
|
+
'/search': { query?: string };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// hooks/use-typed-router.ts
|
|
123
|
+
import { router } from 'expo-router';
|
|
124
|
+
|
|
125
|
+
export function useTypedRouter() {
|
|
126
|
+
return {
|
|
127
|
+
push: <T extends keyof AppRoutes>(
|
|
128
|
+
route: T,
|
|
129
|
+
params?: AppRoutes[T]
|
|
130
|
+
) => {
|
|
131
|
+
router.push({ pathname: route, params } as any);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Usage
|
|
137
|
+
const { push } = useTypedRouter();
|
|
138
|
+
push('/users/[id]', { id: '123' });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### NAV-006: With params and options
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Navigate with params
|
|
145
|
+
router.push({
|
|
146
|
+
pathname: '/users/[id]',
|
|
147
|
+
params: { id: '123' },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// With Link component
|
|
151
|
+
import { Link } from 'expo-router';
|
|
152
|
+
|
|
153
|
+
<Link href="/settings" asChild>
|
|
154
|
+
<Pressable>
|
|
155
|
+
<Text>Settings</Text>
|
|
156
|
+
</Pressable>
|
|
157
|
+
</Link>
|
|
158
|
+
|
|
159
|
+
<Link
|
|
160
|
+
href={{
|
|
161
|
+
pathname: '/users/[id]',
|
|
162
|
+
params: { id: user.id },
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<Text>{user.name}</Text>
|
|
166
|
+
</Link>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Route Parameters
|
|
172
|
+
|
|
173
|
+
### NAV-007: Access route params
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// app/users/[id].tsx
|
|
177
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
178
|
+
|
|
179
|
+
export default function UserScreen() {
|
|
180
|
+
// Type the params
|
|
181
|
+
const { id } = useLocalSearchParams<{ id: string }>();
|
|
182
|
+
|
|
183
|
+
// Use in query
|
|
184
|
+
const { data } = useQuery({
|
|
185
|
+
queryKey: ['user', id],
|
|
186
|
+
queryFn: () => fetchUser(id),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return <UserProfile user={data} />;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### NAV-008: Query parameters
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// URL: /search?query=hello&category=books
|
|
197
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
198
|
+
|
|
199
|
+
export default function SearchScreen() {
|
|
200
|
+
const { query, category } = useLocalSearchParams<{
|
|
201
|
+
query?: string;
|
|
202
|
+
category?: string;
|
|
203
|
+
}>();
|
|
204
|
+
|
|
205
|
+
// Handle arrays (multiple values)
|
|
206
|
+
const tags = useLocalSearchParams<{ tags: string[] }>();
|
|
207
|
+
// URL: /search?tags=a&tags=b -> tags.tags = ['a', 'b']
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### NAV-009: Catch-all routes
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// app/docs/[...slug].tsx
|
|
215
|
+
// Matches /docs/a, /docs/a/b, /docs/a/b/c
|
|
216
|
+
|
|
217
|
+
export default function DocsScreen() {
|
|
218
|
+
const { slug } = useLocalSearchParams<{ slug: string[] }>();
|
|
219
|
+
// /docs/api/auth -> slug = ['api', 'auth']
|
|
220
|
+
|
|
221
|
+
return <DocViewer path={slug.join('/')} />;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Protected Routes
|
|
228
|
+
|
|
229
|
+
### NAV-010: Auth-protected layouts
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// app/(protected)/_layout.tsx
|
|
233
|
+
import { Redirect, Stack } from 'expo-router';
|
|
234
|
+
import { useAuth } from '@/hooks/use-auth';
|
|
235
|
+
|
|
236
|
+
export default function ProtectedLayout() {
|
|
237
|
+
const { user, isLoading } = useAuth();
|
|
238
|
+
|
|
239
|
+
if (isLoading) {
|
|
240
|
+
return <LoadingScreen />;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!user) {
|
|
244
|
+
return <Redirect href="/login" />;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<Stack>
|
|
249
|
+
<Stack.Screen name="dashboard" />
|
|
250
|
+
<Stack.Screen name="settings" />
|
|
251
|
+
</Stack>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### NAV-011: Redirect after auth
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// app/(auth)/login.tsx
|
|
260
|
+
import { router, useLocalSearchParams } from 'expo-router';
|
|
261
|
+
|
|
262
|
+
export default function LoginScreen() {
|
|
263
|
+
const { redirect } = useLocalSearchParams<{ redirect?: string }>();
|
|
264
|
+
|
|
265
|
+
const handleLogin = async (credentials) => {
|
|
266
|
+
await login(credentials);
|
|
267
|
+
|
|
268
|
+
// Redirect to original destination or home
|
|
269
|
+
router.replace(redirect ?? '/(tabs)/home');
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return <LoginForm onSubmit={handleLogin} />;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// When redirecting to login
|
|
276
|
+
router.push({
|
|
277
|
+
pathname: '/login',
|
|
278
|
+
params: { redirect: '/settings' },
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Modals & Sheets
|
|
285
|
+
|
|
286
|
+
### NAV-012: Modal presentation
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// app/_layout.tsx
|
|
290
|
+
<Stack>
|
|
291
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
292
|
+
<Stack.Screen
|
|
293
|
+
name="modal"
|
|
294
|
+
options={{
|
|
295
|
+
presentation: 'modal',
|
|
296
|
+
animation: 'slide_from_bottom',
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
<Stack.Screen
|
|
300
|
+
name="sheet"
|
|
301
|
+
options={{
|
|
302
|
+
presentation: 'transparentModal',
|
|
303
|
+
animation: 'fade',
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
</Stack>
|
|
307
|
+
|
|
308
|
+
// Open modal
|
|
309
|
+
router.push('/modal');
|
|
310
|
+
|
|
311
|
+
// Dismiss
|
|
312
|
+
router.dismiss();
|
|
313
|
+
// or
|
|
314
|
+
router.back();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### NAV-013: Full-screen modals
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// app/create-post.tsx
|
|
321
|
+
export default function CreatePostModal() {
|
|
322
|
+
return (
|
|
323
|
+
<View className="flex-1 bg-white">
|
|
324
|
+
<View className="flex-row justify-between p-4 border-b">
|
|
325
|
+
<Pressable onPress={() => router.dismiss()}>
|
|
326
|
+
<Text>Cancel</Text>
|
|
327
|
+
</Pressable>
|
|
328
|
+
<Pressable onPress={handleSave}>
|
|
329
|
+
<Text className="font-bold text-blue-500">Post</Text>
|
|
330
|
+
</Pressable>
|
|
331
|
+
</View>
|
|
332
|
+
<PostForm />
|
|
333
|
+
</View>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Deep Linking
|
|
341
|
+
|
|
342
|
+
### NAV-014: Configure deep links
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
// app.json
|
|
346
|
+
{
|
|
347
|
+
"expo": {
|
|
348
|
+
"scheme": "myapp",
|
|
349
|
+
"web": {
|
|
350
|
+
"bundler": "metro"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Links:
|
|
356
|
+
// myapp://users/123 -> /users/123
|
|
357
|
+
// myapp://settings -> /settings
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### NAV-015: Handle incoming links
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// app/_layout.tsx
|
|
364
|
+
import { useURL } from 'expo-linking';
|
|
365
|
+
import { router } from 'expo-router';
|
|
366
|
+
|
|
367
|
+
export default function RootLayout() {
|
|
368
|
+
const url = useURL();
|
|
369
|
+
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (url) {
|
|
372
|
+
// Expo Router handles most routing automatically
|
|
373
|
+
// Custom handling if needed:
|
|
374
|
+
const { path, queryParams } = Linking.parse(url);
|
|
375
|
+
|
|
376
|
+
if (path?.startsWith('invite/')) {
|
|
377
|
+
// Handle invite links specially
|
|
378
|
+
handleInvite(queryParams?.code);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}, [url]);
|
|
382
|
+
|
|
383
|
+
return <Stack />;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Navigation State
|
|
390
|
+
|
|
391
|
+
### NAV-016: Track navigation state
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { usePathname, useSegments } from 'expo-router';
|
|
395
|
+
|
|
396
|
+
function NavigationTracker() {
|
|
397
|
+
const pathname = usePathname();
|
|
398
|
+
const segments = useSegments();
|
|
399
|
+
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
// Analytics
|
|
402
|
+
analytics.screen(pathname);
|
|
403
|
+
}, [pathname]);
|
|
404
|
+
|
|
405
|
+
// segments = ['(tabs)', 'home'] for /(tabs)/home
|
|
406
|
+
const isInTabs = segments[0] === '(tabs)';
|
|
407
|
+
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### NAV-017: Prevent back navigation
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { useNavigation } from 'expo-router';
|
|
416
|
+
import { useEffect } from 'react';
|
|
417
|
+
|
|
418
|
+
export default function CheckoutScreen() {
|
|
419
|
+
const navigation = useNavigation();
|
|
420
|
+
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
// Prevent hardware back on Android
|
|
423
|
+
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
|
|
424
|
+
if (hasUnsavedChanges) {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
// Show confirmation
|
|
427
|
+
Alert.alert(
|
|
428
|
+
'Discard changes?',
|
|
429
|
+
'You have unsaved changes.',
|
|
430
|
+
[
|
|
431
|
+
{ text: 'Stay', style: 'cancel' },
|
|
432
|
+
{
|
|
433
|
+
text: 'Discard',
|
|
434
|
+
style: 'destructive',
|
|
435
|
+
onPress: () => navigation.dispatch(e.data.action),
|
|
436
|
+
},
|
|
437
|
+
]
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
return unsubscribe;
|
|
443
|
+
}, [hasUnsavedChanges]);
|
|
444
|
+
}
|
|
445
|
+
```
|