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,427 @@
|
|
|
1
|
+
# Critical Rules - Expo/React Native
|
|
2
|
+
|
|
3
|
+
Must-follow rules. Violations block PR merge.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Component Patterns
|
|
8
|
+
|
|
9
|
+
### RN-001: Use correct components
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// BAD - Web components don't work
|
|
13
|
+
<div className="container">
|
|
14
|
+
<span>Text</span>
|
|
15
|
+
<img src={url} />
|
|
16
|
+
|
|
17
|
+
// GOOD - React Native components
|
|
18
|
+
<View className="container">
|
|
19
|
+
<Text>Text content</Text>
|
|
20
|
+
<Image source={{ uri: url }} />
|
|
21
|
+
|
|
22
|
+
// Text must be wrapped
|
|
23
|
+
// BAD
|
|
24
|
+
<View>Hello World</View>
|
|
25
|
+
|
|
26
|
+
// GOOD
|
|
27
|
+
<View><Text>Hello World</Text></View>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### RN-002: Handle text properly
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// All text must be in Text component
|
|
34
|
+
// BAD
|
|
35
|
+
<View>{user.name}</View>
|
|
36
|
+
|
|
37
|
+
// GOOD
|
|
38
|
+
<View><Text>{user.name}</Text></View>
|
|
39
|
+
|
|
40
|
+
// Nested text is OK
|
|
41
|
+
<Text>
|
|
42
|
+
Hello <Text className="font-bold">{name}</Text>!
|
|
43
|
+
</Text>
|
|
44
|
+
|
|
45
|
+
// String interpolation OK in Text
|
|
46
|
+
<Text>{`Welcome, ${name}`}</Text>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### RN-003: Use Pressable for touch
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// GOOD - Pressable with feedback
|
|
53
|
+
<Pressable
|
|
54
|
+
onPress={handlePress}
|
|
55
|
+
className="active:opacity-70"
|
|
56
|
+
hitSlop={8} // Increase touch target
|
|
57
|
+
>
|
|
58
|
+
<Text>Tap me</Text>
|
|
59
|
+
</Pressable>
|
|
60
|
+
|
|
61
|
+
// With proper states
|
|
62
|
+
<Pressable
|
|
63
|
+
onPress={handlePress}
|
|
64
|
+
className={({ pressed }) =>
|
|
65
|
+
`p-4 rounded-lg ${pressed ? 'bg-gray-200' : 'bg-white'}`
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
{({ pressed }) => (
|
|
69
|
+
<Text className={pressed ? 'text-gray-600' : 'text-black'}>
|
|
70
|
+
Tap me
|
|
71
|
+
</Text>
|
|
72
|
+
)}
|
|
73
|
+
</Pressable>
|
|
74
|
+
|
|
75
|
+
// Avoid TouchableOpacity (deprecated feel)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Platform Safety
|
|
81
|
+
|
|
82
|
+
### RN-004: Handle platform differences
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Platform } from 'react-native';
|
|
86
|
+
|
|
87
|
+
// Platform-specific code
|
|
88
|
+
if (Platform.OS === 'ios') {
|
|
89
|
+
// iOS only
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Platform.OS === 'android') {
|
|
93
|
+
// Android only
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Platform.select for values
|
|
97
|
+
const styles = {
|
|
98
|
+
shadow: Platform.select({
|
|
99
|
+
ios: {
|
|
100
|
+
shadowColor: '#000',
|
|
101
|
+
shadowOffset: { width: 0, height: 2 },
|
|
102
|
+
shadowOpacity: 0.1,
|
|
103
|
+
shadowRadius: 4,
|
|
104
|
+
},
|
|
105
|
+
android: {
|
|
106
|
+
elevation: 4,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### RN-005: Safe area handling
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
116
|
+
|
|
117
|
+
// Wrap screens that need safe area
|
|
118
|
+
export default function HomeScreen() {
|
|
119
|
+
return (
|
|
120
|
+
<SafeAreaView className="flex-1 bg-white" edges={['top']}>
|
|
121
|
+
<ScrollView>
|
|
122
|
+
{/* Content */}
|
|
123
|
+
</ScrollView>
|
|
124
|
+
</SafeAreaView>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Or use safe area hooks
|
|
129
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
130
|
+
|
|
131
|
+
function Header() {
|
|
132
|
+
const insets = useSafeAreaInsets();
|
|
133
|
+
return (
|
|
134
|
+
<View style={{ paddingTop: insets.top }}>
|
|
135
|
+
{/* Header content */}
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### RN-006: Keyboard handling
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { KeyboardAvoidingView, Platform } from 'react-native';
|
|
145
|
+
|
|
146
|
+
function LoginScreen() {
|
|
147
|
+
return (
|
|
148
|
+
<KeyboardAvoidingView
|
|
149
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
150
|
+
className="flex-1"
|
|
151
|
+
>
|
|
152
|
+
<ScrollView keyboardShouldPersistTaps="handled">
|
|
153
|
+
<TextInput placeholder="Email" />
|
|
154
|
+
<TextInput placeholder="Password" secureTextEntry />
|
|
155
|
+
<Button title="Login" onPress={handleLogin} />
|
|
156
|
+
</ScrollView>
|
|
157
|
+
</KeyboardAvoidingView>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Performance
|
|
165
|
+
|
|
166
|
+
### RN-007: Avoid inline styles in lists
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// BAD - Creates new object every render
|
|
170
|
+
<FlatList
|
|
171
|
+
data={items}
|
|
172
|
+
renderItem={({ item }) => (
|
|
173
|
+
<View style={{ padding: 16, margin: 8 }}> {/* New object! */}
|
|
174
|
+
<Text>{item.name}</Text>
|
|
175
|
+
</View>
|
|
176
|
+
)}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
// GOOD - Use className or StyleSheet
|
|
180
|
+
<FlatList
|
|
181
|
+
data={items}
|
|
182
|
+
renderItem={({ item }) => (
|
|
183
|
+
<View className="p-4 m-2">
|
|
184
|
+
<Text>{item.name}</Text>
|
|
185
|
+
</View>
|
|
186
|
+
)}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
// Or memoized styles
|
|
190
|
+
const styles = StyleSheet.create({
|
|
191
|
+
item: { padding: 16, margin: 8 },
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### RN-008: Use FlashList for long lists
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { FlashList } from '@shopify/flash-list';
|
|
199
|
+
|
|
200
|
+
// GOOD - Better performance than FlatList
|
|
201
|
+
<FlashList
|
|
202
|
+
data={items}
|
|
203
|
+
keyExtractor={(item) => item.id}
|
|
204
|
+
renderItem={({ item }) => <ItemCard item={item} />}
|
|
205
|
+
estimatedItemSize={80} // Required!
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
// FlatList OK for short lists (<50 items)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### RN-009: Memoize expensive renders
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
215
|
+
|
|
216
|
+
// Memoize list items
|
|
217
|
+
const ItemCard = memo(function ItemCard({ item, onPress }) {
|
|
218
|
+
return (
|
|
219
|
+
<Pressable onPress={() => onPress(item.id)}>
|
|
220
|
+
<Text>{item.name}</Text>
|
|
221
|
+
</Pressable>
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Memoize callbacks
|
|
226
|
+
function ItemList({ items }) {
|
|
227
|
+
const handlePress = useCallback((id: string) => {
|
|
228
|
+
router.push(`/items/${id}`);
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
// Memoize computed values
|
|
232
|
+
const sortedItems = useMemo(
|
|
233
|
+
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
|
234
|
+
[items]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<FlashList
|
|
239
|
+
data={sortedItems}
|
|
240
|
+
renderItem={({ item }) => (
|
|
241
|
+
<ItemCard item={item} onPress={handlePress} />
|
|
242
|
+
)}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Security
|
|
251
|
+
|
|
252
|
+
### RN-010: Secure storage for sensitive data
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// BAD - AsyncStorage for tokens
|
|
256
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
257
|
+
await AsyncStorage.setItem('token', token); // Not encrypted!
|
|
258
|
+
|
|
259
|
+
// GOOD - SecureStore for sensitive data
|
|
260
|
+
import * as SecureStore from 'expo-secure-store';
|
|
261
|
+
await SecureStore.setItemAsync('token', token); // Encrypted
|
|
262
|
+
|
|
263
|
+
// Use AsyncStorage only for non-sensitive data
|
|
264
|
+
await AsyncStorage.setItem('theme', 'dark'); // OK
|
|
265
|
+
await AsyncStorage.setItem('onboarded', 'true'); // OK
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### RN-011: Validate deep links
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// app.json
|
|
272
|
+
{
|
|
273
|
+
"expo": {
|
|
274
|
+
"scheme": "myapp"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Validate incoming links
|
|
279
|
+
import { useURL } from 'expo-linking';
|
|
280
|
+
|
|
281
|
+
function DeepLinkHandler() {
|
|
282
|
+
const url = useURL();
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (url) {
|
|
286
|
+
const parsed = Linking.parse(url);
|
|
287
|
+
|
|
288
|
+
// Validate before navigating
|
|
289
|
+
if (isValidPath(parsed.path)) {
|
|
290
|
+
router.push(parsed.path);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}, [url]);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### RN-012: Never hardcode secrets
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// BAD
|
|
301
|
+
const API_KEY = 'sk_live_xxxxx';
|
|
302
|
+
|
|
303
|
+
// GOOD - Use environment variables
|
|
304
|
+
// app.config.js
|
|
305
|
+
export default {
|
|
306
|
+
expo: {
|
|
307
|
+
extra: {
|
|
308
|
+
apiUrl: process.env.API_URL,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Access via Constants
|
|
314
|
+
import Constants from 'expo-constants';
|
|
315
|
+
const apiUrl = Constants.expoConfig?.extra?.apiUrl;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Accessibility
|
|
321
|
+
|
|
322
|
+
### RN-013: Add accessibility labels
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
<Pressable
|
|
326
|
+
onPress={handleDelete}
|
|
327
|
+
accessibilityLabel="Delete item"
|
|
328
|
+
accessibilityRole="button"
|
|
329
|
+
accessibilityHint="Removes this item from your list"
|
|
330
|
+
>
|
|
331
|
+
<TrashIcon />
|
|
332
|
+
</Pressable>
|
|
333
|
+
|
|
334
|
+
<Image
|
|
335
|
+
source={{ uri: user.avatar }}
|
|
336
|
+
accessibilityLabel={`Profile picture of ${user.name}`}
|
|
337
|
+
/>
|
|
338
|
+
|
|
339
|
+
// For decorative elements
|
|
340
|
+
<Image
|
|
341
|
+
source={decorativeImage}
|
|
342
|
+
accessibilityElementsHidden={true}
|
|
343
|
+
/>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### RN-014: Support screen readers
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// Group related elements
|
|
350
|
+
<View accessibilityRole="summary">
|
|
351
|
+
<Text>Order Total</Text>
|
|
352
|
+
<Text>${total}</Text>
|
|
353
|
+
</View>
|
|
354
|
+
|
|
355
|
+
// Live regions for updates
|
|
356
|
+
<View
|
|
357
|
+
accessibilityLiveRegion="polite"
|
|
358
|
+
accessibilityLabel={`${count} items in cart`}
|
|
359
|
+
>
|
|
360
|
+
<Text>{count}</Text>
|
|
361
|
+
</View>
|
|
362
|
+
|
|
363
|
+
// State changes
|
|
364
|
+
<Pressable
|
|
365
|
+
accessibilityState={{ selected: isSelected }}
|
|
366
|
+
accessibilityRole="checkbox"
|
|
367
|
+
>
|
|
368
|
+
<Text>Option {isSelected ? '(selected)' : ''}</Text>
|
|
369
|
+
</Pressable>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Error Handling
|
|
375
|
+
|
|
376
|
+
### RN-015: Handle errors gracefully
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
380
|
+
|
|
381
|
+
function ErrorFallback({ error, resetErrorBoundary }) {
|
|
382
|
+
return (
|
|
383
|
+
<View className="flex-1 items-center justify-center p-4">
|
|
384
|
+
<Text className="text-lg font-bold text-red-500">
|
|
385
|
+
Something went wrong
|
|
386
|
+
</Text>
|
|
387
|
+
<Text className="text-gray-600 mt-2">{error.message}</Text>
|
|
388
|
+
<Pressable
|
|
389
|
+
onPress={resetErrorBoundary}
|
|
390
|
+
className="mt-4 bg-blue-500 px-4 py-2 rounded"
|
|
391
|
+
>
|
|
392
|
+
<Text className="text-white">Try Again</Text>
|
|
393
|
+
</Pressable>
|
|
394
|
+
</View>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Wrap screens
|
|
399
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
400
|
+
<HomeScreen />
|
|
401
|
+
</ErrorBoundary>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### RN-016: Handle network errors
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import NetInfo from '@react-native-community/netinfo';
|
|
408
|
+
|
|
409
|
+
function useNetworkStatus() {
|
|
410
|
+
const [isConnected, setIsConnected] = useState(true);
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
return NetInfo.addEventListener((state) => {
|
|
414
|
+
setIsConnected(state.isConnected ?? true);
|
|
415
|
+
});
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
return isConnected;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// In components
|
|
422
|
+
const isConnected = useNetworkStatus();
|
|
423
|
+
|
|
424
|
+
if (!isConnected) {
|
|
425
|
+
return <OfflineScreen />;
|
|
426
|
+
}
|
|
427
|
+
```
|