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,455 @@
|
|
|
1
|
+
# Native Features - Expo SDK
|
|
2
|
+
|
|
3
|
+
Patterns for native APIs and device features.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Storage
|
|
8
|
+
|
|
9
|
+
### NATIVE-001: Secure storage for sensitive data
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import * as SecureStore from 'expo-secure-store';
|
|
13
|
+
|
|
14
|
+
const secureStorage = {
|
|
15
|
+
async get(key: string): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
return await SecureStore.getItemAsync(key);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async set(key: string, value: string): Promise<void> {
|
|
24
|
+
await SecureStore.setItemAsync(key, value);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async remove(key: string): Promise<void> {
|
|
28
|
+
await SecureStore.deleteItemAsync(key);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Use for: tokens, credentials, sensitive user data
|
|
33
|
+
await secureStorage.set('auth_token', token);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### NATIVE-002: AsyncStorage for preferences
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
40
|
+
|
|
41
|
+
const storage = {
|
|
42
|
+
async get<T>(key: string): Promise<T | null> {
|
|
43
|
+
const value = await AsyncStorage.getItem(key);
|
|
44
|
+
return value ? JSON.parse(value) : null;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
48
|
+
await AsyncStorage.setItem(key, JSON.stringify(value));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async remove(key: string): Promise<void> {
|
|
52
|
+
await AsyncStorage.removeItem(key);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Use for: preferences, cache, non-sensitive data
|
|
57
|
+
await storage.set('theme', 'dark');
|
|
58
|
+
await storage.set('onboarded', true);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Camera & Images
|
|
64
|
+
|
|
65
|
+
### NATIVE-003: Image picker
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
69
|
+
|
|
70
|
+
async function pickImage() {
|
|
71
|
+
// Request permission
|
|
72
|
+
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
73
|
+
|
|
74
|
+
if (status !== 'granted') {
|
|
75
|
+
Alert.alert('Permission needed', 'Please allow access to photos.');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
80
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
81
|
+
allowsEditing: true,
|
|
82
|
+
aspect: [1, 1],
|
|
83
|
+
quality: 0.8,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (result.canceled) return null;
|
|
87
|
+
|
|
88
|
+
return result.assets[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function takePhoto() {
|
|
92
|
+
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
|
93
|
+
|
|
94
|
+
if (status !== 'granted') {
|
|
95
|
+
Alert.alert('Permission needed', 'Please allow camera access.');
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await ImagePicker.launchCameraAsync({
|
|
100
|
+
allowsEditing: true,
|
|
101
|
+
aspect: [1, 1],
|
|
102
|
+
quality: 0.8,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (result.canceled) return null;
|
|
106
|
+
|
|
107
|
+
return result.assets[0];
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### NATIVE-004: Image upload
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
async function uploadImage(uri: string) {
|
|
115
|
+
const formData = new FormData();
|
|
116
|
+
|
|
117
|
+
formData.append('image', {
|
|
118
|
+
uri,
|
|
119
|
+
type: 'image/jpeg',
|
|
120
|
+
name: 'photo.jpg',
|
|
121
|
+
} as any);
|
|
122
|
+
|
|
123
|
+
const response = await fetch('/api/upload', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body: formData,
|
|
126
|
+
headers: {
|
|
127
|
+
'Content-Type': 'multipart/form-data',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return response.json();
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Location
|
|
138
|
+
|
|
139
|
+
### NATIVE-005: Get current location
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import * as Location from 'expo-location';
|
|
143
|
+
|
|
144
|
+
async function getCurrentLocation() {
|
|
145
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
146
|
+
|
|
147
|
+
if (status !== 'granted') {
|
|
148
|
+
throw new Error('Location permission denied');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const location = await Location.getCurrentPositionAsync({
|
|
152
|
+
accuracy: Location.Accuracy.Balanced,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
latitude: location.coords.latitude,
|
|
157
|
+
longitude: location.coords.longitude,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### NATIVE-006: Watch location
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import * as Location from 'expo-location';
|
|
166
|
+
|
|
167
|
+
function useLocation() {
|
|
168
|
+
const [location, setLocation] = useState<Location.LocationObject | null>(null);
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
let subscription: Location.LocationSubscription;
|
|
172
|
+
|
|
173
|
+
(async () => {
|
|
174
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
175
|
+
if (status !== 'granted') return;
|
|
176
|
+
|
|
177
|
+
subscription = await Location.watchPositionAsync(
|
|
178
|
+
{
|
|
179
|
+
accuracy: Location.Accuracy.High,
|
|
180
|
+
timeInterval: 5000,
|
|
181
|
+
distanceInterval: 10,
|
|
182
|
+
},
|
|
183
|
+
(newLocation) => {
|
|
184
|
+
setLocation(newLocation);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
})();
|
|
188
|
+
|
|
189
|
+
return () => subscription?.remove();
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
return location;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Push Notifications
|
|
199
|
+
|
|
200
|
+
### NATIVE-007: Register for notifications
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import * as Notifications from 'expo-notifications';
|
|
204
|
+
import * as Device from 'expo-device';
|
|
205
|
+
import { Platform } from 'react-native';
|
|
206
|
+
|
|
207
|
+
// Configure notification handling
|
|
208
|
+
Notifications.setNotificationHandler({
|
|
209
|
+
handleNotification: async () => ({
|
|
210
|
+
shouldShowAlert: true,
|
|
211
|
+
shouldPlaySound: true,
|
|
212
|
+
shouldSetBadge: true,
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
async function registerForPushNotifications() {
|
|
217
|
+
if (!Device.isDevice) {
|
|
218
|
+
console.log('Push notifications require a physical device');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
|
223
|
+
let finalStatus = existingStatus;
|
|
224
|
+
|
|
225
|
+
if (existingStatus !== 'granted') {
|
|
226
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
227
|
+
finalStatus = status;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (finalStatus !== 'granted') {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Android channel
|
|
235
|
+
if (Platform.OS === 'android') {
|
|
236
|
+
await Notifications.setNotificationChannelAsync('default', {
|
|
237
|
+
name: 'Default',
|
|
238
|
+
importance: Notifications.AndroidImportance.MAX,
|
|
239
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const token = await Notifications.getExpoPushTokenAsync({
|
|
244
|
+
projectId: 'your-project-id',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return token.data;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### NATIVE-008: Handle notifications
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import * as Notifications from 'expo-notifications';
|
|
255
|
+
import { router } from 'expo-router';
|
|
256
|
+
|
|
257
|
+
function useNotificationHandlers() {
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
// Notification received while app is foregrounded
|
|
260
|
+
const foregroundSubscription = Notifications.addNotificationReceivedListener(
|
|
261
|
+
(notification) => {
|
|
262
|
+
console.log('Notification received:', notification);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// User tapped notification
|
|
267
|
+
const responseSubscription = Notifications.addNotificationResponseReceivedListener(
|
|
268
|
+
(response) => {
|
|
269
|
+
const data = response.notification.request.content.data;
|
|
270
|
+
|
|
271
|
+
// Navigate based on notification data
|
|
272
|
+
if (data.screen) {
|
|
273
|
+
router.push(data.screen as string);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return () => {
|
|
279
|
+
foregroundSubscription.remove();
|
|
280
|
+
responseSubscription.remove();
|
|
281
|
+
};
|
|
282
|
+
}, []);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Haptics
|
|
289
|
+
|
|
290
|
+
### NATIVE-009: Haptic feedback
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import * as Haptics from 'expo-haptics';
|
|
294
|
+
|
|
295
|
+
// Light - selections, toggles
|
|
296
|
+
function handleSelect() {
|
|
297
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Medium - button presses
|
|
301
|
+
function handlePress() {
|
|
302
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Heavy - significant actions
|
|
306
|
+
function handleDrop() {
|
|
307
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Success/error feedback
|
|
311
|
+
function handleSuccess() {
|
|
312
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleError() {
|
|
316
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Selection changed (subtle)
|
|
320
|
+
function handleSelectionChange() {
|
|
321
|
+
Haptics.selectionAsync();
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Biometrics
|
|
328
|
+
|
|
329
|
+
### NATIVE-010: Biometric authentication
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import * as LocalAuthentication from 'expo-local-authentication';
|
|
333
|
+
|
|
334
|
+
async function authenticateWithBiometrics() {
|
|
335
|
+
// Check hardware support
|
|
336
|
+
const hasHardware = await LocalAuthentication.hasHardwareAsync();
|
|
337
|
+
if (!hasHardware) {
|
|
338
|
+
return { success: false, error: 'No biometric hardware' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check enrollment
|
|
342
|
+
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
|
|
343
|
+
if (!isEnrolled) {
|
|
344
|
+
return { success: false, error: 'No biometrics enrolled' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Authenticate
|
|
348
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
349
|
+
promptMessage: 'Authenticate to continue',
|
|
350
|
+
cancelLabel: 'Cancel',
|
|
351
|
+
disableDeviceFallback: false, // Allow PIN fallback
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return { success: result.success, error: result.error };
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Share & Clipboard
|
|
361
|
+
|
|
362
|
+
### NATIVE-011: Share content
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { Share } from 'react-native';
|
|
366
|
+
|
|
367
|
+
async function shareContent(title: string, message: string, url?: string) {
|
|
368
|
+
try {
|
|
369
|
+
await Share.share({
|
|
370
|
+
title,
|
|
371
|
+
message: url ? `${message}\n${url}` : message,
|
|
372
|
+
url, // iOS only
|
|
373
|
+
});
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('Share failed:', error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Usage
|
|
380
|
+
shareContent('Check this out!', 'Amazing article', 'https://example.com');
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### NATIVE-012: Clipboard
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import * as Clipboard from 'expo-clipboard';
|
|
387
|
+
|
|
388
|
+
async function copyToClipboard(text: string) {
|
|
389
|
+
await Clipboard.setStringAsync(text);
|
|
390
|
+
// Show toast/feedback
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function pasteFromClipboard(): Promise<string> {
|
|
394
|
+
return Clipboard.getStringAsync();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// With haptic feedback
|
|
398
|
+
async function copyWithFeedback(text: string) {
|
|
399
|
+
await Clipboard.setStringAsync(text);
|
|
400
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
401
|
+
Toast.show('Copied!');
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Device Info
|
|
408
|
+
|
|
409
|
+
### NATIVE-013: Device information
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import * as Device from 'expo-device';
|
|
413
|
+
import { Platform } from 'react-native';
|
|
414
|
+
|
|
415
|
+
const deviceInfo = {
|
|
416
|
+
brand: Device.brand,
|
|
417
|
+
modelName: Device.modelName,
|
|
418
|
+
osName: Device.osName,
|
|
419
|
+
osVersion: Device.osVersion,
|
|
420
|
+
isDevice: Device.isDevice,
|
|
421
|
+
platform: Platform.OS,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Check if physical device
|
|
425
|
+
if (!Device.isDevice) {
|
|
426
|
+
console.log('Running in simulator/emulator');
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### NATIVE-014: App state
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { useAppState } from '@react-native-community/hooks';
|
|
434
|
+
import { AppState } from 'react-native';
|
|
435
|
+
|
|
436
|
+
function useAppStateListener(callback: (state: string) => void) {
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
const subscription = AppState.addEventListener('change', callback);
|
|
439
|
+
return () => subscription.remove();
|
|
440
|
+
}, [callback]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Refresh data when app comes to foreground
|
|
444
|
+
function DataRefresher() {
|
|
445
|
+
const queryClient = useQueryClient();
|
|
446
|
+
|
|
447
|
+
useAppStateListener((state) => {
|
|
448
|
+
if (state === 'active') {
|
|
449
|
+
queryClient.invalidateQueries();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
```
|