omgkit 2.0.6 → 2.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/package.json +6 -3
- package/plugin/agents/architect.md +357 -43
- package/plugin/agents/code-reviewer.md +481 -22
- package/plugin/agents/debugger.md +397 -30
- package/plugin/agents/docs-manager.md +431 -23
- package/plugin/agents/fullstack-developer.md +395 -34
- package/plugin/agents/git-manager.md +438 -20
- package/plugin/agents/oracle.md +329 -53
- package/plugin/agents/planner.md +275 -32
- package/plugin/agents/researcher.md +343 -21
- package/plugin/agents/scout.md +423 -18
- package/plugin/agents/sprint-master.md +418 -48
- package/plugin/agents/tester.md +551 -26
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-development
|
|
3
|
+
description: Cross-platform mobile development with React Native and Expo including navigation, state management, and native features
|
|
4
|
+
category: mobile
|
|
5
|
+
triggers:
|
|
6
|
+
- mobile development
|
|
7
|
+
- react native
|
|
8
|
+
- expo
|
|
9
|
+
- cross platform
|
|
10
|
+
- mobile app
|
|
11
|
+
- ios android
|
|
12
|
+
- native features
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Mobile Development
|
|
16
|
+
|
|
17
|
+
Build **cross-platform mobile applications** with React Native and Expo. This skill covers component architecture, navigation patterns, state management, and native feature integration.
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Create production-ready mobile applications:
|
|
22
|
+
|
|
23
|
+
- Build iOS and Android apps from single codebase
|
|
24
|
+
- Implement native navigation patterns
|
|
25
|
+
- Manage application state effectively
|
|
26
|
+
- Access device features (camera, location, notifications)
|
|
27
|
+
- Handle offline-first architecture
|
|
28
|
+
- Optimize performance for mobile devices
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. Expo Project Setup
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Create new Expo project
|
|
36
|
+
npx create-expo-app@latest my-app --template tabs
|
|
37
|
+
|
|
38
|
+
# Project structure
|
|
39
|
+
my-app/
|
|
40
|
+
├── app/ # File-based routing
|
|
41
|
+
│ ├── (tabs)/ # Tab navigation group
|
|
42
|
+
│ │ ├── _layout.tsx # Tab layout
|
|
43
|
+
│ │ ├── index.tsx # Home tab
|
|
44
|
+
│ │ └── profile.tsx # Profile tab
|
|
45
|
+
│ ├── _layout.tsx # Root layout
|
|
46
|
+
│ └── modal.tsx # Modal screen
|
|
47
|
+
├── components/ # Reusable components
|
|
48
|
+
├── hooks/ # Custom hooks
|
|
49
|
+
├── services/ # API services
|
|
50
|
+
├── store/ # State management
|
|
51
|
+
├── constants/ # App constants
|
|
52
|
+
└── assets/ # Images, fonts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// app/_layout.tsx - Root layout with providers
|
|
57
|
+
import { Stack } from 'expo-router';
|
|
58
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
59
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
60
|
+
|
|
61
|
+
const queryClient = new QueryClient();
|
|
62
|
+
|
|
63
|
+
export default function RootLayout() {
|
|
64
|
+
return (
|
|
65
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
66
|
+
<QueryClientProvider client={queryClient}>
|
|
67
|
+
<AuthProvider>
|
|
68
|
+
<ThemeProvider>
|
|
69
|
+
<Stack>
|
|
70
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
71
|
+
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
|
72
|
+
</Stack>
|
|
73
|
+
</ThemeProvider>
|
|
74
|
+
</AuthProvider>
|
|
75
|
+
</QueryClientProvider>
|
|
76
|
+
</GestureHandlerRootView>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Navigation Patterns
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Tab Navigation with Expo Router
|
|
85
|
+
// app/(tabs)/_layout.tsx
|
|
86
|
+
import { Tabs } from 'expo-router';
|
|
87
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
88
|
+
|
|
89
|
+
export default function TabLayout() {
|
|
90
|
+
const colorScheme = useColorScheme();
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Tabs
|
|
94
|
+
screenOptions={{
|
|
95
|
+
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
|
96
|
+
headerShown: false,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Tabs.Screen
|
|
100
|
+
name="index"
|
|
101
|
+
options={{
|
|
102
|
+
title: 'Home',
|
|
103
|
+
tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
<Tabs.Screen
|
|
107
|
+
name="explore"
|
|
108
|
+
options={{
|
|
109
|
+
title: 'Explore',
|
|
110
|
+
tabBarIcon: ({ color }) => <Ionicons name="compass" size={24} color={color} />,
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<Tabs.Screen
|
|
114
|
+
name="profile"
|
|
115
|
+
options={{
|
|
116
|
+
title: 'Profile',
|
|
117
|
+
tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} />,
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
</Tabs>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Stack Navigation with authentication
|
|
125
|
+
// app/(auth)/_layout.tsx
|
|
126
|
+
import { Stack, Redirect } from 'expo-router';
|
|
127
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
128
|
+
|
|
129
|
+
export default function AuthLayout() {
|
|
130
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
131
|
+
|
|
132
|
+
if (isLoading) {
|
|
133
|
+
return <LoadingScreen />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!isAuthenticated) {
|
|
137
|
+
return <Redirect href="/login" />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Stack>
|
|
142
|
+
<Stack.Screen name="dashboard" options={{ title: 'Dashboard' }} />
|
|
143
|
+
<Stack.Screen
|
|
144
|
+
name="settings"
|
|
145
|
+
options={{
|
|
146
|
+
title: 'Settings',
|
|
147
|
+
presentation: 'modal',
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
</Stack>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Deep linking configuration
|
|
155
|
+
// app.json
|
|
156
|
+
{
|
|
157
|
+
"expo": {
|
|
158
|
+
"scheme": "myapp",
|
|
159
|
+
"web": {
|
|
160
|
+
"bundler": "metro"
|
|
161
|
+
},
|
|
162
|
+
"plugins": [
|
|
163
|
+
[
|
|
164
|
+
"expo-router",
|
|
165
|
+
{
|
|
166
|
+
"origin": "https://myapp.com"
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 3. State Management with Zustand
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// store/useStore.ts
|
|
178
|
+
import { create } from 'zustand';
|
|
179
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
180
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
181
|
+
|
|
182
|
+
interface User {
|
|
183
|
+
id: string;
|
|
184
|
+
name: string;
|
|
185
|
+
email: string;
|
|
186
|
+
avatar?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface AuthState {
|
|
190
|
+
user: User | null;
|
|
191
|
+
token: string | null;
|
|
192
|
+
isAuthenticated: boolean;
|
|
193
|
+
login: (user: User, token: string) => void;
|
|
194
|
+
logout: () => void;
|
|
195
|
+
updateUser: (updates: Partial<User>) => void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const useAuthStore = create<AuthState>()(
|
|
199
|
+
persist(
|
|
200
|
+
(set) => ({
|
|
201
|
+
user: null,
|
|
202
|
+
token: null,
|
|
203
|
+
isAuthenticated: false,
|
|
204
|
+
|
|
205
|
+
login: (user, token) => set({
|
|
206
|
+
user,
|
|
207
|
+
token,
|
|
208
|
+
isAuthenticated: true,
|
|
209
|
+
}),
|
|
210
|
+
|
|
211
|
+
logout: () => set({
|
|
212
|
+
user: null,
|
|
213
|
+
token: null,
|
|
214
|
+
isAuthenticated: false,
|
|
215
|
+
}),
|
|
216
|
+
|
|
217
|
+
updateUser: (updates) => set((state) => ({
|
|
218
|
+
user: state.user ? { ...state.user, ...updates } : null,
|
|
219
|
+
})),
|
|
220
|
+
}),
|
|
221
|
+
{
|
|
222
|
+
name: 'auth-storage',
|
|
223
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Cart store with computed values
|
|
229
|
+
interface CartItem {
|
|
230
|
+
id: string;
|
|
231
|
+
productId: string;
|
|
232
|
+
name: string;
|
|
233
|
+
price: number;
|
|
234
|
+
quantity: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface CartState {
|
|
238
|
+
items: CartItem[];
|
|
239
|
+
addItem: (item: Omit<CartItem, 'id'>) => void;
|
|
240
|
+
removeItem: (id: string) => void;
|
|
241
|
+
updateQuantity: (id: string, quantity: number) => void;
|
|
242
|
+
clearCart: () => void;
|
|
243
|
+
total: () => number;
|
|
244
|
+
itemCount: () => number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const useCartStore = create<CartState>((set, get) => ({
|
|
248
|
+
items: [],
|
|
249
|
+
|
|
250
|
+
addItem: (item) => set((state) => {
|
|
251
|
+
const existing = state.items.find(i => i.productId === item.productId);
|
|
252
|
+
if (existing) {
|
|
253
|
+
return {
|
|
254
|
+
items: state.items.map(i =>
|
|
255
|
+
i.productId === item.productId
|
|
256
|
+
? { ...i, quantity: i.quantity + item.quantity }
|
|
257
|
+
: i
|
|
258
|
+
),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
items: [...state.items, { ...item, id: generateId() }],
|
|
263
|
+
};
|
|
264
|
+
}),
|
|
265
|
+
|
|
266
|
+
removeItem: (id) => set((state) => ({
|
|
267
|
+
items: state.items.filter(i => i.id !== id),
|
|
268
|
+
})),
|
|
269
|
+
|
|
270
|
+
updateQuantity: (id, quantity) => set((state) => ({
|
|
271
|
+
items: quantity > 0
|
|
272
|
+
? state.items.map(i => i.id === id ? { ...i, quantity } : i)
|
|
273
|
+
: state.items.filter(i => i.id !== id),
|
|
274
|
+
})),
|
|
275
|
+
|
|
276
|
+
clearCart: () => set({ items: [] }),
|
|
277
|
+
|
|
278
|
+
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
279
|
+
|
|
280
|
+
itemCount: () => get().items.reduce((count, item) => count + item.quantity, 0),
|
|
281
|
+
}));
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 4. API Integration with React Query
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// services/api.ts
|
|
288
|
+
import axios from 'axios';
|
|
289
|
+
import { useAuthStore } from '@/store/useStore';
|
|
290
|
+
|
|
291
|
+
const api = axios.create({
|
|
292
|
+
baseURL: process.env.EXPO_PUBLIC_API_URL,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Request interceptor for auth
|
|
296
|
+
api.interceptors.request.use((config) => {
|
|
297
|
+
const token = useAuthStore.getState().token;
|
|
298
|
+
if (token) {
|
|
299
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
300
|
+
}
|
|
301
|
+
return config;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Response interceptor for errors
|
|
305
|
+
api.interceptors.response.use(
|
|
306
|
+
(response) => response,
|
|
307
|
+
async (error) => {
|
|
308
|
+
if (error.response?.status === 401) {
|
|
309
|
+
useAuthStore.getState().logout();
|
|
310
|
+
}
|
|
311
|
+
return Promise.reject(error);
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// hooks/useProducts.ts
|
|
316
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
317
|
+
|
|
318
|
+
export function useProducts(categoryId?: string) {
|
|
319
|
+
return useQuery({
|
|
320
|
+
queryKey: ['products', categoryId],
|
|
321
|
+
queryFn: async () => {
|
|
322
|
+
const params = categoryId ? { category: categoryId } : {};
|
|
323
|
+
const { data } = await api.get('/products', { params });
|
|
324
|
+
return data;
|
|
325
|
+
},
|
|
326
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function useProduct(id: string) {
|
|
331
|
+
return useQuery({
|
|
332
|
+
queryKey: ['product', id],
|
|
333
|
+
queryFn: async () => {
|
|
334
|
+
const { data } = await api.get(`/products/${id}`);
|
|
335
|
+
return data;
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function useCreateOrder() {
|
|
341
|
+
const queryClient = useQueryClient();
|
|
342
|
+
|
|
343
|
+
return useMutation({
|
|
344
|
+
mutationFn: async (orderData: CreateOrderInput) => {
|
|
345
|
+
const { data } = await api.post('/orders', orderData);
|
|
346
|
+
return data;
|
|
347
|
+
},
|
|
348
|
+
onSuccess: () => {
|
|
349
|
+
queryClient.invalidateQueries({ queryKey: ['orders'] });
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Infinite scroll with pagination
|
|
355
|
+
export function useInfiniteProducts() {
|
|
356
|
+
return useInfiniteQuery({
|
|
357
|
+
queryKey: ['products', 'infinite'],
|
|
358
|
+
queryFn: async ({ pageParam = 1 }) => {
|
|
359
|
+
const { data } = await api.get('/products', {
|
|
360
|
+
params: { page: pageParam, limit: 20 },
|
|
361
|
+
});
|
|
362
|
+
return data;
|
|
363
|
+
},
|
|
364
|
+
getNextPageParam: (lastPage) =>
|
|
365
|
+
lastPage.hasMore ? lastPage.page + 1 : undefined,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 5. Native Features
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// Camera integration
|
|
374
|
+
import { Camera, CameraType } from 'expo-camera';
|
|
375
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
376
|
+
|
|
377
|
+
export function CameraScreen() {
|
|
378
|
+
const [permission, requestPermission] = Camera.useCameraPermissions();
|
|
379
|
+
const [type, setType] = useState(CameraType.back);
|
|
380
|
+
const cameraRef = useRef<Camera>(null);
|
|
381
|
+
|
|
382
|
+
async function takePicture() {
|
|
383
|
+
if (cameraRef.current) {
|
|
384
|
+
const photo = await cameraRef.current.takePictureAsync({
|
|
385
|
+
quality: 0.8,
|
|
386
|
+
base64: false,
|
|
387
|
+
});
|
|
388
|
+
// Handle photo
|
|
389
|
+
await uploadImage(photo.uri);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function pickImage() {
|
|
394
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
395
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
396
|
+
allowsEditing: true,
|
|
397
|
+
aspect: [4, 3],
|
|
398
|
+
quality: 0.8,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (!result.canceled) {
|
|
402
|
+
await uploadImage(result.assets[0].uri);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!permission?.granted) {
|
|
407
|
+
return (
|
|
408
|
+
<View style={styles.container}>
|
|
409
|
+
<Text>Camera permission required</Text>
|
|
410
|
+
<Button title="Grant Permission" onPress={requestPermission} />
|
|
411
|
+
</View>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<View style={styles.container}>
|
|
417
|
+
<Camera style={styles.camera} type={type} ref={cameraRef}>
|
|
418
|
+
<View style={styles.controls}>
|
|
419
|
+
<TouchableOpacity onPress={() => setType(
|
|
420
|
+
type === CameraType.back ? CameraType.front : CameraType.back
|
|
421
|
+
)}>
|
|
422
|
+
<Ionicons name="camera-reverse" size={32} color="white" />
|
|
423
|
+
</TouchableOpacity>
|
|
424
|
+
<TouchableOpacity onPress={takePicture}>
|
|
425
|
+
<View style={styles.captureButton} />
|
|
426
|
+
</TouchableOpacity>
|
|
427
|
+
<TouchableOpacity onPress={pickImage}>
|
|
428
|
+
<Ionicons name="images" size={32} color="white" />
|
|
429
|
+
</TouchableOpacity>
|
|
430
|
+
</View>
|
|
431
|
+
</Camera>
|
|
432
|
+
</View>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Location services
|
|
437
|
+
import * as Location from 'expo-location';
|
|
438
|
+
|
|
439
|
+
export function useLocation() {
|
|
440
|
+
const [location, setLocation] = useState<Location.LocationObject | null>(null);
|
|
441
|
+
const [error, setError] = useState<string | null>(null);
|
|
442
|
+
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
(async () => {
|
|
445
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
446
|
+
if (status !== 'granted') {
|
|
447
|
+
setError('Location permission denied');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const currentLocation = await Location.getCurrentPositionAsync({
|
|
452
|
+
accuracy: Location.Accuracy.Balanced,
|
|
453
|
+
});
|
|
454
|
+
setLocation(currentLocation);
|
|
455
|
+
})();
|
|
456
|
+
}, []);
|
|
457
|
+
|
|
458
|
+
return { location, error };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Push notifications
|
|
462
|
+
import * as Notifications from 'expo-notifications';
|
|
463
|
+
import * as Device from 'expo-device';
|
|
464
|
+
|
|
465
|
+
Notifications.setNotificationHandler({
|
|
466
|
+
handleNotification: async () => ({
|
|
467
|
+
shouldShowAlert: true,
|
|
468
|
+
shouldPlaySound: true,
|
|
469
|
+
shouldSetBadge: true,
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
export async function registerForPushNotifications(): Promise<string | null> {
|
|
474
|
+
if (!Device.isDevice) {
|
|
475
|
+
console.log('Push notifications require a physical device');
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
|
480
|
+
let finalStatus = existingStatus;
|
|
481
|
+
|
|
482
|
+
if (existingStatus !== 'granted') {
|
|
483
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
484
|
+
finalStatus = status;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (finalStatus !== 'granted') {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const token = await Notifications.getExpoPushTokenAsync({
|
|
492
|
+
projectId: process.env.EXPO_PUBLIC_PROJECT_ID,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return token.data;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function usePushNotifications() {
|
|
499
|
+
const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
|
|
500
|
+
const notificationListener = useRef<Notifications.Subscription>();
|
|
501
|
+
const responseListener = useRef<Notifications.Subscription>();
|
|
502
|
+
|
|
503
|
+
useEffect(() => {
|
|
504
|
+
registerForPushNotifications().then(setExpoPushToken);
|
|
505
|
+
|
|
506
|
+
notificationListener.current = Notifications.addNotificationReceivedListener(
|
|
507
|
+
(notification) => {
|
|
508
|
+
console.log('Notification received:', notification);
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
responseListener.current = Notifications.addNotificationResponseReceivedListener(
|
|
513
|
+
(response) => {
|
|
514
|
+
const data = response.notification.request.content.data;
|
|
515
|
+
// Handle notification tap
|
|
516
|
+
if (data.screen) {
|
|
517
|
+
router.push(data.screen);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return () => {
|
|
523
|
+
notificationListener.current?.remove();
|
|
524
|
+
responseListener.current?.remove();
|
|
525
|
+
};
|
|
526
|
+
}, []);
|
|
527
|
+
|
|
528
|
+
return { expoPushToken };
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### 6. Performance Optimization
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// Optimized FlatList with virtualization
|
|
536
|
+
import { FlashList } from '@shopify/flash-list';
|
|
537
|
+
|
|
538
|
+
interface ProductListProps {
|
|
539
|
+
products: Product[];
|
|
540
|
+
onEndReached: () => void;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function ProductList({ products, onEndReached }: ProductListProps) {
|
|
544
|
+
const renderItem = useCallback(({ item }: { item: Product }) => (
|
|
545
|
+
<ProductCard product={item} />
|
|
546
|
+
), []);
|
|
547
|
+
|
|
548
|
+
const keyExtractor = useCallback((item: Product) => item.id, []);
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<FlashList
|
|
552
|
+
data={products}
|
|
553
|
+
renderItem={renderItem}
|
|
554
|
+
keyExtractor={keyExtractor}
|
|
555
|
+
estimatedItemSize={200}
|
|
556
|
+
onEndReached={onEndReached}
|
|
557
|
+
onEndReachedThreshold={0.5}
|
|
558
|
+
ItemSeparatorComponent={Separator}
|
|
559
|
+
ListEmptyComponent={EmptyState}
|
|
560
|
+
/>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Memoized components
|
|
565
|
+
const ProductCard = memo(function ProductCard({ product }: { product: Product }) {
|
|
566
|
+
const navigation = useNavigation();
|
|
567
|
+
|
|
568
|
+
const handlePress = useCallback(() => {
|
|
569
|
+
navigation.navigate('ProductDetail', { id: product.id });
|
|
570
|
+
}, [product.id, navigation]);
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
<Pressable onPress={handlePress} style={styles.card}>
|
|
574
|
+
<Image
|
|
575
|
+
source={{ uri: product.image }}
|
|
576
|
+
style={styles.image}
|
|
577
|
+
contentFit="cover"
|
|
578
|
+
transition={200}
|
|
579
|
+
cachePolicy="memory-disk"
|
|
580
|
+
/>
|
|
581
|
+
<Text style={styles.name}>{product.name}</Text>
|
|
582
|
+
<Text style={styles.price}>${product.price}</Text>
|
|
583
|
+
</Pressable>
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Image optimization with expo-image
|
|
588
|
+
import { Image } from 'expo-image';
|
|
589
|
+
|
|
590
|
+
const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7telephones';
|
|
591
|
+
|
|
592
|
+
export function OptimizedImage({ source, style }) {
|
|
593
|
+
return (
|
|
594
|
+
<Image
|
|
595
|
+
source={source}
|
|
596
|
+
style={style}
|
|
597
|
+
placeholder={blurhash}
|
|
598
|
+
contentFit="cover"
|
|
599
|
+
transition={300}
|
|
600
|
+
cachePolicy="memory-disk"
|
|
601
|
+
/>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Skeleton loading
|
|
606
|
+
import { Skeleton } from 'moti/skeleton';
|
|
607
|
+
|
|
608
|
+
export function ProductCardSkeleton() {
|
|
609
|
+
return (
|
|
610
|
+
<View style={styles.card}>
|
|
611
|
+
<Skeleton colorMode="light" width="100%" height={200} />
|
|
612
|
+
<Skeleton colorMode="light" width="80%" height={20} />
|
|
613
|
+
<Skeleton colorMode="light" width="40%" height={16} />
|
|
614
|
+
</View>
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Use Cases
|
|
620
|
+
|
|
621
|
+
### 1. E-commerce App
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
// Complete product screen
|
|
625
|
+
export function ProductScreen() {
|
|
626
|
+
const { id } = useLocalSearchParams();
|
|
627
|
+
const { data: product, isLoading } = useProduct(id as string);
|
|
628
|
+
const addToCart = useCartStore((state) => state.addItem);
|
|
629
|
+
|
|
630
|
+
if (isLoading) return <ProductSkeleton />;
|
|
631
|
+
if (!product) return <NotFound />;
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<ScrollView>
|
|
635
|
+
<ImageGallery images={product.images} />
|
|
636
|
+
|
|
637
|
+
<View style={styles.content}>
|
|
638
|
+
<Text style={styles.name}>{product.name}</Text>
|
|
639
|
+
<Text style={styles.price}>${product.price}</Text>
|
|
640
|
+
|
|
641
|
+
<VariantSelector
|
|
642
|
+
variants={product.variants}
|
|
643
|
+
onSelect={setSelectedVariant}
|
|
644
|
+
/>
|
|
645
|
+
|
|
646
|
+
<Button
|
|
647
|
+
title="Add to Cart"
|
|
648
|
+
onPress={() => addToCart({
|
|
649
|
+
productId: product.id,
|
|
650
|
+
name: product.name,
|
|
651
|
+
price: product.price,
|
|
652
|
+
quantity: 1,
|
|
653
|
+
})}
|
|
654
|
+
/>
|
|
655
|
+
|
|
656
|
+
<Text style={styles.description}>{product.description}</Text>
|
|
657
|
+
</View>
|
|
658
|
+
</ScrollView>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 2. Social App with Real-time
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// Real-time chat with Socket.io
|
|
667
|
+
import { io } from 'socket.io-client';
|
|
668
|
+
|
|
669
|
+
export function useChatRoom(roomId: string) {
|
|
670
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
671
|
+
const socketRef = useRef<Socket>();
|
|
672
|
+
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
const token = useAuthStore.getState().token;
|
|
675
|
+
|
|
676
|
+
socketRef.current = io(process.env.EXPO_PUBLIC_WS_URL!, {
|
|
677
|
+
auth: { token },
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
socketRef.current.emit('join', roomId);
|
|
681
|
+
|
|
682
|
+
socketRef.current.on('message', (message: Message) => {
|
|
683
|
+
setMessages((prev) => [...prev, message]);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
return () => {
|
|
687
|
+
socketRef.current?.emit('leave', roomId);
|
|
688
|
+
socketRef.current?.disconnect();
|
|
689
|
+
};
|
|
690
|
+
}, [roomId]);
|
|
691
|
+
|
|
692
|
+
const sendMessage = useCallback((content: string) => {
|
|
693
|
+
socketRef.current?.emit('message', { roomId, content });
|
|
694
|
+
}, [roomId]);
|
|
695
|
+
|
|
696
|
+
return { messages, sendMessage };
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
## Best Practices
|
|
701
|
+
|
|
702
|
+
### Do's
|
|
703
|
+
|
|
704
|
+
- **Use Expo for faster development** - Managed workflow handles complexity
|
|
705
|
+
- **Implement offline-first** - Use AsyncStorage and optimistic updates
|
|
706
|
+
- **Optimize images** - Use expo-image with caching
|
|
707
|
+
- **Use FlashList** - Better performance than FlatList
|
|
708
|
+
- **Test on real devices** - Simulators don't show real performance
|
|
709
|
+
- **Handle all permission states** - Request gracefully
|
|
710
|
+
|
|
711
|
+
### Don'ts
|
|
712
|
+
|
|
713
|
+
- Don't block the JS thread with heavy computations
|
|
714
|
+
- Don't use inline styles in render methods
|
|
715
|
+
- Don't forget to handle keyboard avoiding
|
|
716
|
+
- Don't ignore deep linking setup
|
|
717
|
+
- Don't skip splash screen configuration
|
|
718
|
+
- Don't neglect accessibility
|
|
719
|
+
|
|
720
|
+
### Performance Checklist
|
|
721
|
+
|
|
722
|
+
```markdown
|
|
723
|
+
## Mobile Performance Checklist
|
|
724
|
+
|
|
725
|
+
### Rendering
|
|
726
|
+
- [ ] Use memo for expensive components
|
|
727
|
+
- [ ] Implement proper list virtualization
|
|
728
|
+
- [ ] Optimize images (size, format, caching)
|
|
729
|
+
- [ ] Avoid inline function props
|
|
730
|
+
|
|
731
|
+
### State
|
|
732
|
+
- [ ] Split stores by domain
|
|
733
|
+
- [ ] Use selectors for derived state
|
|
734
|
+
- [ ] Persist critical data
|
|
735
|
+
- [ ] Handle loading/error states
|
|
736
|
+
|
|
737
|
+
### Network
|
|
738
|
+
- [ ] Implement request caching
|
|
739
|
+
- [ ] Use optimistic updates
|
|
740
|
+
- [ ] Handle offline gracefully
|
|
741
|
+
- [ ] Implement retry logic
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Related Skills
|
|
745
|
+
|
|
746
|
+
- **react** - React fundamentals
|
|
747
|
+
- **typescript** - Type-safe development
|
|
748
|
+
- **frontend-design** - UI/UX patterns
|
|
749
|
+
- **api-architecture** - Backend integration
|
|
750
|
+
|
|
751
|
+
## Reference Resources
|
|
752
|
+
|
|
753
|
+
- [Expo Documentation](https://docs.expo.dev/)
|
|
754
|
+
- [React Native Docs](https://reactnative.dev/)
|
|
755
|
+
- [React Navigation](https://reactnavigation.org/)
|
|
756
|
+
- [Expo Router](https://docs.expo.dev/router/introduction/)
|