@buivietphi/skill-mobile-mt 2.1.0 → 2.2.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/AGENTS.md +83 -45
- package/README.md +44 -35
- package/SKILL.md +333 -46
- package/package.json +1 -1
- package/shared/code-generation-templates.md +656 -0
- package/shared/code-review.md +8 -8
- package/shared/complex-ui-patterns.md +526 -0
- package/shared/data-flow-patterns.md +422 -0
- package/shared/error-handling.md +394 -0
- package/shared/intent-analysis.md +473 -0
- package/shared/navigation-patterns.md +375 -0
- package/shared/spec-to-code.md +293 -0
- package/shared/testing-patterns.md +428 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Navigation Patterns — Complex Flows
|
|
2
|
+
|
|
3
|
+
> On-demand module. Loaded when implementing auth flows, deep links, modals, or tab navigation.
|
|
4
|
+
> Contains production patterns for React Native, Flutter, iOS, and Android.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Auth-Based Navigation Flow
|
|
9
|
+
|
|
10
|
+
### React Native (React Navigation)
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// navigation/RootNavigator.tsx
|
|
14
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
15
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
16
|
+
import { useAuthStore, useIsLoggedIn } from '@/stores/useAuthStore';
|
|
17
|
+
|
|
18
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
19
|
+
|
|
20
|
+
export function RootNavigator() {
|
|
21
|
+
const isLoggedIn = useIsLoggedIn();
|
|
22
|
+
const [isReady, setIsReady] = useState(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Check stored token on app start
|
|
26
|
+
async function bootstrap() {
|
|
27
|
+
const hasToken = useAuthStore.getState().token;
|
|
28
|
+
if (hasToken) {
|
|
29
|
+
const valid = await useAuthStore.getState().refreshSession();
|
|
30
|
+
if (!valid) useAuthStore.getState().logout();
|
|
31
|
+
}
|
|
32
|
+
setIsReady(true);
|
|
33
|
+
}
|
|
34
|
+
bootstrap();
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (!isReady) return <SplashScreen />;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<NavigationContainer>
|
|
41
|
+
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
|
42
|
+
{isLoggedIn ? (
|
|
43
|
+
// Authenticated stack
|
|
44
|
+
<>
|
|
45
|
+
<Stack.Screen name="MainTabs" component={MainTabNavigator} />
|
|
46
|
+
<Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
|
|
47
|
+
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
48
|
+
{/* Modals */}
|
|
49
|
+
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
|
50
|
+
<Stack.Screen name="EditProfile" component={EditProfileScreen} />
|
|
51
|
+
<Stack.Screen name="ImageViewer" component={ImageViewerScreen} />
|
|
52
|
+
</Stack.Group>
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
// Unauthenticated stack
|
|
56
|
+
<>
|
|
57
|
+
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
|
|
58
|
+
<Stack.Screen name="Login" component={LoginScreen} />
|
|
59
|
+
<Stack.Screen name="Register" component={RegisterScreen} />
|
|
60
|
+
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</Stack.Navigator>
|
|
64
|
+
</NavigationContainer>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Flutter (GoRouter)
|
|
70
|
+
|
|
71
|
+
```dart
|
|
72
|
+
// navigation/app_router.dart
|
|
73
|
+
import 'package:go_router/go_router.dart';
|
|
74
|
+
|
|
75
|
+
final appRouter = GoRouter(
|
|
76
|
+
initialLocation: '/',
|
|
77
|
+
redirect: (context, state) {
|
|
78
|
+
final isLoggedIn = ref.read(authProvider).isLoggedIn;
|
|
79
|
+
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
|
80
|
+
|
|
81
|
+
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
|
|
82
|
+
if (isLoggedIn && isAuthRoute) return '/';
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
routes: [
|
|
86
|
+
// Auth routes
|
|
87
|
+
GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()),
|
|
88
|
+
GoRoute(path: '/auth/register', builder: (_, __) => const RegisterScreen()),
|
|
89
|
+
|
|
90
|
+
// App routes with bottom nav shell
|
|
91
|
+
ShellRoute(
|
|
92
|
+
builder: (_, __, child) => ScaffoldWithNavBar(child: child),
|
|
93
|
+
routes: [
|
|
94
|
+
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
|
|
95
|
+
GoRoute(path: '/search', builder: (_, __) => const SearchScreen()),
|
|
96
|
+
GoRoute(path: '/cart', builder: (_, __) => const CartScreen()),
|
|
97
|
+
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
|
|
98
|
+
],
|
|
99
|
+
),
|
|
100
|
+
|
|
101
|
+
// Detail routes (no bottom nav)
|
|
102
|
+
GoRoute(
|
|
103
|
+
path: '/product/:id',
|
|
104
|
+
builder: (_, state) => ProductDetailScreen(id: state.pathParameters['id']!),
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Deep Linking
|
|
113
|
+
|
|
114
|
+
### React Native (Expo Router)
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// app/_layout.tsx — Expo Router handles deep links automatically via file structure
|
|
118
|
+
// URL: myapp://product/abc-123 → app/product/[id].tsx
|
|
119
|
+
|
|
120
|
+
// app/product/[id].tsx
|
|
121
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
122
|
+
|
|
123
|
+
export default function ProductDetailScreen() {
|
|
124
|
+
const { id } = useLocalSearchParams<{ id: string }>();
|
|
125
|
+
|
|
126
|
+
// Validate param exists
|
|
127
|
+
if (!id) return <NotFoundScreen />;
|
|
128
|
+
|
|
129
|
+
// Fetch and render
|
|
130
|
+
const { data, isLoading } = useProductDetail(id as ProductId);
|
|
131
|
+
// ...
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### React Navigation Deep Link Config
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// navigation/linking.ts
|
|
139
|
+
const linking: LinkingOptions<RootStackParamList> = {
|
|
140
|
+
prefixes: ['myapp://', 'https://myapp.com'],
|
|
141
|
+
config: {
|
|
142
|
+
screens: {
|
|
143
|
+
MainTabs: {
|
|
144
|
+
screens: {
|
|
145
|
+
Home: 'home',
|
|
146
|
+
Profile: 'profile/:userId',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
ProductDetail: 'product/:productId',
|
|
150
|
+
Settings: 'settings',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Handle notification deep links
|
|
156
|
+
import * as Notifications from 'expo-notifications';
|
|
157
|
+
|
|
158
|
+
function useNotificationDeepLink() {
|
|
159
|
+
const navigation = useAppNavigation();
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const sub = Notifications.addNotificationResponseReceivedListener(response => {
|
|
163
|
+
const data = response.notification.request.content.data;
|
|
164
|
+
if (data.screen === 'ProductDetail' && data.productId) {
|
|
165
|
+
navigation.navigate('ProductDetail', { productId: data.productId as ProductId });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return () => sub.remove();
|
|
169
|
+
}, [navigation]);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Bottom Tab Navigation
|
|
176
|
+
|
|
177
|
+
### React Native — Lazy Tabs with State Preservation
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// navigation/MainTabNavigator.tsx
|
|
181
|
+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
182
|
+
|
|
183
|
+
const Tab = createBottomTabNavigator<MainTabParamList>();
|
|
184
|
+
|
|
185
|
+
export function MainTabNavigator() {
|
|
186
|
+
return (
|
|
187
|
+
<Tab.Navigator
|
|
188
|
+
screenOptions={{
|
|
189
|
+
headerShown: false,
|
|
190
|
+
// Lazy load: render tab only when first visited
|
|
191
|
+
lazy: true,
|
|
192
|
+
// Freeze inactive tabs (prevent re-renders)
|
|
193
|
+
freezeOnBlur: true,
|
|
194
|
+
tabBarActiveTintColor: theme.colors.primary,
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<Tab.Screen
|
|
198
|
+
name="Home"
|
|
199
|
+
component={HomeScreen}
|
|
200
|
+
options={{
|
|
201
|
+
tabBarIcon: ({ color, size }) => <HomeIcon color={color} size={size} />,
|
|
202
|
+
tabBarLabel: 'Home',
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
<Tab.Screen
|
|
206
|
+
name="Search"
|
|
207
|
+
component={SearchScreen}
|
|
208
|
+
options={{
|
|
209
|
+
tabBarIcon: ({ color, size }) => <SearchIcon color={color} size={size} />,
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
<Tab.Screen
|
|
213
|
+
name="Cart"
|
|
214
|
+
component={CartScreen}
|
|
215
|
+
options={{
|
|
216
|
+
tabBarIcon: ({ color, size }) => <CartIcon color={color} size={size} />,
|
|
217
|
+
tabBarBadge: cartCount > 0 ? cartCount : undefined,
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
<Tab.Screen
|
|
221
|
+
name="Profile"
|
|
222
|
+
component={ProfileScreen}
|
|
223
|
+
options={{
|
|
224
|
+
tabBarIcon: ({ color, size }) => <ProfileIcon color={color} size={size} />,
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
</Tab.Navigator>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Modal Navigation
|
|
235
|
+
|
|
236
|
+
### React Native — Modal Stack
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Present as modal (slides up from bottom on iOS)
|
|
240
|
+
navigation.navigate('EditProfile'); // registered in modal group
|
|
241
|
+
|
|
242
|
+
// Dismiss modal
|
|
243
|
+
navigation.goBack();
|
|
244
|
+
|
|
245
|
+
// Modal with result — pass callback via params or use event
|
|
246
|
+
// Option A: Use navigation params
|
|
247
|
+
navigation.navigate('SelectAddress', {
|
|
248
|
+
onSelect: (address: Address) => {
|
|
249
|
+
// handle selected address
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Option B: Use event emitter
|
|
254
|
+
import { DeviceEventEmitter } from 'react-native';
|
|
255
|
+
// In modal: DeviceEventEmitter.emit('addressSelected', address);
|
|
256
|
+
// In parent: DeviceEventEmitter.addListener('addressSelected', handler);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Flutter — Modal Bottom Sheet
|
|
260
|
+
|
|
261
|
+
```dart
|
|
262
|
+
// Modal bottom sheet
|
|
263
|
+
showModalBottomSheet(
|
|
264
|
+
context: context,
|
|
265
|
+
isScrollControlled: true, // full-height if needed
|
|
266
|
+
useSafeArea: true,
|
|
267
|
+
builder: (context) => DraggableScrollableSheet(
|
|
268
|
+
initialChildSize: 0.6,
|
|
269
|
+
minChildSize: 0.3,
|
|
270
|
+
maxChildSize: 0.9,
|
|
271
|
+
builder: (_, controller) => AddressPickerSheet(scrollController: controller),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Push Notification Navigation
|
|
279
|
+
|
|
280
|
+
### React Native (Expo Notifications)
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// hooks/useNotificationSetup.ts
|
|
284
|
+
import * as Notifications from 'expo-notifications';
|
|
285
|
+
import * as Device from 'expo-device';
|
|
286
|
+
|
|
287
|
+
export function useNotificationSetup() {
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
registerForPush();
|
|
290
|
+
}, []);
|
|
291
|
+
|
|
292
|
+
async function registerForPush() {
|
|
293
|
+
if (!Device.isDevice) return; // skip simulator
|
|
294
|
+
|
|
295
|
+
const { status } = await Notifications.getPermissionsAsync();
|
|
296
|
+
let finalStatus = status;
|
|
297
|
+
|
|
298
|
+
if (status !== 'granted') {
|
|
299
|
+
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
|
300
|
+
finalStatus = newStatus;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (finalStatus !== 'granted') return;
|
|
304
|
+
|
|
305
|
+
const token = (await Notifications.getExpoPushTokenAsync()).data;
|
|
306
|
+
await userService.registerPushToken(token);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Notification handler — runs when app receives notification
|
|
311
|
+
Notifications.setNotificationHandler({
|
|
312
|
+
handleNotification: async () => ({
|
|
313
|
+
shouldShowAlert: true,
|
|
314
|
+
shouldPlaySound: true,
|
|
315
|
+
shouldSetBadge: true,
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Permissions Handling Pattern
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// hooks/usePermission.ts
|
|
326
|
+
import * as Location from 'expo-location';
|
|
327
|
+
import * as Camera from 'expo-camera';
|
|
328
|
+
import { Alert, Linking } from 'react-native';
|
|
329
|
+
|
|
330
|
+
type PermissionType = 'camera' | 'location' | 'notifications';
|
|
331
|
+
|
|
332
|
+
export function usePermission(type: PermissionType) {
|
|
333
|
+
const [granted, setGranted] = useState<boolean | null>(null);
|
|
334
|
+
|
|
335
|
+
const request = useCallback(async () => {
|
|
336
|
+
let result: { status: string };
|
|
337
|
+
|
|
338
|
+
switch (type) {
|
|
339
|
+
case 'camera':
|
|
340
|
+
result = await Camera.requestCameraPermissionsAsync();
|
|
341
|
+
break;
|
|
342
|
+
case 'location':
|
|
343
|
+
result = await Location.requestForegroundPermissionsAsync();
|
|
344
|
+
break;
|
|
345
|
+
case 'notifications':
|
|
346
|
+
result = await Notifications.requestPermissionsAsync();
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (result.status === 'granted') {
|
|
351
|
+
setGranted(true);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Permission denied — guide user to settings
|
|
356
|
+
Alert.alert(
|
|
357
|
+
'Permission Required',
|
|
358
|
+
`Please enable ${type} access in Settings to use this feature.`,
|
|
359
|
+
[
|
|
360
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
361
|
+
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
|
|
362
|
+
]
|
|
363
|
+
);
|
|
364
|
+
setGranted(false);
|
|
365
|
+
return false;
|
|
366
|
+
}, [type]);
|
|
367
|
+
|
|
368
|
+
return { granted, request };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Usage:
|
|
372
|
+
// const camera = usePermission('camera');
|
|
373
|
+
// const canUse = await camera.request();
|
|
374
|
+
// if (canUse) { /* proceed */ }
|
|
375
|
+
```
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Spec-to-Code — From Requirements to Implementation
|
|
2
|
+
|
|
3
|
+
> On-demand module. Loaded when building new features from specs, user stories, or vague descriptions.
|
|
4
|
+
> Bridges the gap between "what to build" and "how to implement it".
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Spec → Code Pipeline
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
STEP 1: PARSE SPEC (extract structured requirements)
|
|
12
|
+
STEP 2: DEPENDENCY GRAPH (map what depends on what)
|
|
13
|
+
STEP 3: FILE PLAN (which files to create/modify)
|
|
14
|
+
STEP 4: TYPE DEFINITIONS (interfaces + branded types)
|
|
15
|
+
STEP 5: IMPLEMENT (bottom-up: types → services → hooks → screens)
|
|
16
|
+
STEP 6: VERIFY (against original spec checklist)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Step 1: Parse Spec → Structured Requirements
|
|
22
|
+
|
|
23
|
+
Given ANY feature description, extract these 8 items:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────┐
|
|
27
|
+
│ 1. ENTITY What data objects? │
|
|
28
|
+
│ 2. FIELDS What properties each? │
|
|
29
|
+
│ 3. ACTIONS What can user do? │
|
|
30
|
+
│ 4. STATES Loading/error/empty/ok │
|
|
31
|
+
│ 5. NAVIGATION From where? To where? │
|
|
32
|
+
│ 6. API Which endpoints? │
|
|
33
|
+
│ 7. STORAGE Persist anything local? │
|
|
34
|
+
│ 8. VALIDATION Input rules? │
|
|
35
|
+
└─────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Step 2: Dependency Graph Template
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
[FeatureName]Screen
|
|
44
|
+
├── Components
|
|
45
|
+
│ ├── [Name]Header
|
|
46
|
+
│ ├── [Name]List / [Name]Card
|
|
47
|
+
│ ├── [Name]Form (if editable)
|
|
48
|
+
│ └── [Name]Empty / [Name]Error / [Name]Skeleton
|
|
49
|
+
│
|
|
50
|
+
├── Hook: use[FeatureName]
|
|
51
|
+
│ ├── Query: use[Entity]Query (GET data)
|
|
52
|
+
│ ├── Mutation: use[Action]Mutation (POST/PUT/DELETE)
|
|
53
|
+
│ └── State: use[Store]Store (local state)
|
|
54
|
+
│
|
|
55
|
+
├── Service: [entity]Service.ts
|
|
56
|
+
│ ├── get[Entity](params) → API call
|
|
57
|
+
│ ├── create[Entity](data) → API call
|
|
58
|
+
│ ├── update[Entity](id, data) → API call
|
|
59
|
+
│ └── delete[Entity](id) → API call
|
|
60
|
+
│
|
|
61
|
+
├── Types: [entity].types.ts
|
|
62
|
+
│ ├── [Entity] interface
|
|
63
|
+
│ ├── Create[Entity]Input
|
|
64
|
+
│ ├── Update[Entity]Input
|
|
65
|
+
│ └── [Entity]Params (filters, pagination)
|
|
66
|
+
│
|
|
67
|
+
└── Navigation: registered in navigator
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 3: File Plan — What Goes Where
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
RULE: Follow existing project structure. NEVER invent new patterns.
|
|
76
|
+
RULE: Scan project for a SIMILAR feature. Clone its file structure.
|
|
77
|
+
|
|
78
|
+
TYPICAL FILE PLAN:
|
|
79
|
+
|
|
80
|
+
src/features/[feature]/
|
|
81
|
+
├── [Feature]Screen.tsx ← Screen component (4 states)
|
|
82
|
+
├── components/
|
|
83
|
+
│ ├── [Feature]Header.tsx ← Header with title + actions
|
|
84
|
+
│ ├── [Feature]List.tsx ← List/grid of items
|
|
85
|
+
│ ├── [Feature]Card.tsx ← Single item card
|
|
86
|
+
│ ├── [Feature]Form.tsx ← Form (if editable)
|
|
87
|
+
│ ├── [Feature]Skeleton.tsx ← Loading skeleton
|
|
88
|
+
│ └── [Feature]Empty.tsx ← Empty state
|
|
89
|
+
├── hooks/
|
|
90
|
+
│ └── use[Feature].ts ← Business logic hook
|
|
91
|
+
├── services/
|
|
92
|
+
│ └── [feature]Service.ts ← API calls
|
|
93
|
+
└── types/
|
|
94
|
+
└── [feature].types.ts ← TypeScript interfaces
|
|
95
|
+
|
|
96
|
+
ALSO UPDATE:
|
|
97
|
+
├── navigation/ ← Register new screen
|
|
98
|
+
└── stores/ (if new store) ← Only if feature needs global state
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Step 4: Type-First Development
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
ALWAYS write types BEFORE implementation.
|
|
107
|
+
|
|
108
|
+
ORDER:
|
|
109
|
+
1. Entity types (what data looks like)
|
|
110
|
+
2. Input types (what user submits)
|
|
111
|
+
3. API response types (what server returns)
|
|
112
|
+
4. Screen param types (navigation params)
|
|
113
|
+
|
|
114
|
+
WHY: Types catch integration errors before runtime.
|
|
115
|
+
Types serve as documentation for the feature.
|
|
116
|
+
Types make code review faster (reviewer reads types first).
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Step 5: Implementation Order (Bottom-Up)
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
WRONG ORDER (causes integration bugs):
|
|
125
|
+
Screen → Hook → Service → Types
|
|
126
|
+
(screen written before knowing what data looks like)
|
|
127
|
+
|
|
128
|
+
RIGHT ORDER:
|
|
129
|
+
1. types/[feature].types.ts ← Define the contract
|
|
130
|
+
2. services/[feature]Service.ts ← Implement API calls
|
|
131
|
+
3. hooks/use[Feature].ts ← Wire service + state
|
|
132
|
+
4. components/ ← Build UI pieces
|
|
133
|
+
5. [Feature]Screen.tsx ← Compose everything
|
|
134
|
+
6. navigation/ ← Register route
|
|
135
|
+
|
|
136
|
+
Each step VERIFIES against the previous:
|
|
137
|
+
Service matches types? ✓
|
|
138
|
+
Hook calls service correctly? ✓
|
|
139
|
+
Component renders hook data? ✓
|
|
140
|
+
Screen composes components? ✓
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Full Walkthrough Example
|
|
146
|
+
|
|
147
|
+
### Spec: "Product Detail Screen"
|
|
148
|
+
|
|
149
|
+
**User says:** "I need a product detail screen showing images, title, price, description, reviews, and an 'Add to Cart' button. Cart persists offline."
|
|
150
|
+
|
|
151
|
+
### Parse:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
1. ENTITY: Product, CartItem, Review
|
|
155
|
+
2. FIELDS:
|
|
156
|
+
Product → id, title, price, description, images[], category, inStock
|
|
157
|
+
CartItem → productId, quantity, price
|
|
158
|
+
Review → id, userId, rating, comment, createdAt
|
|
159
|
+
3. ACTIONS: View product, Add to cart, View reviews, Share
|
|
160
|
+
4. STATES: Loading (skeleton), Error (retry), Empty (404), Success
|
|
161
|
+
5. NAVIGATION: From: ProductList → To: Cart, ReviewList
|
|
162
|
+
6. API: GET /products/:id, POST /cart/items, GET /products/:id/reviews
|
|
163
|
+
7. STORAGE: Cart stored locally (MMKV) for offline
|
|
164
|
+
8. VALIDATION: Quantity ≥ 1, max 99
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Dependency Graph:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
ProductDetailScreen
|
|
171
|
+
├── ImageCarousel ← Horizontal scroll, snap, indicators
|
|
172
|
+
├── ProductInfo ← Title, price, description, stock badge
|
|
173
|
+
├── ReviewSummary ← Average rating, count, "See all" link
|
|
174
|
+
├── AddToCartButton ← Quantity selector + CTA
|
|
175
|
+
│
|
|
176
|
+
├── useProductDetail(id)
|
|
177
|
+
│ ├── useQuery(['product', id], () => productService.getById(id))
|
|
178
|
+
│ └── useQuery(['reviews', id], () => productService.getReviews(id))
|
|
179
|
+
│
|
|
180
|
+
├── useCartStore (Zustand + MMKV persist)
|
|
181
|
+
│ ├── addItem(productId, quantity, price)
|
|
182
|
+
│ ├── removeItem(productId)
|
|
183
|
+
│ └── items: CartItem[]
|
|
184
|
+
│
|
|
185
|
+
├── productService.ts
|
|
186
|
+
│ ├── getById(id: ProductId): Promise<Product>
|
|
187
|
+
│ └── getReviews(id: ProductId): Promise<Review[]>
|
|
188
|
+
│
|
|
189
|
+
└── Types
|
|
190
|
+
├── Product, CartItem, Review
|
|
191
|
+
├── ProductDetailParams = { productId: ProductId }
|
|
192
|
+
└── AddToCartInput = { productId: ProductId; quantity: number }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### File Plan:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
src/features/product/
|
|
199
|
+
├── ProductDetailScreen.tsx
|
|
200
|
+
├── components/
|
|
201
|
+
│ ├── ImageCarousel.tsx
|
|
202
|
+
│ ├── ProductInfo.tsx
|
|
203
|
+
│ ├── ReviewSummary.tsx
|
|
204
|
+
│ ├── AddToCartButton.tsx
|
|
205
|
+
│ └── ProductDetailSkeleton.tsx
|
|
206
|
+
├── hooks/
|
|
207
|
+
│ └── useProductDetail.ts
|
|
208
|
+
├── services/
|
|
209
|
+
│ └── productService.ts
|
|
210
|
+
└── types/
|
|
211
|
+
└── product.types.ts
|
|
212
|
+
|
|
213
|
+
src/stores/useCartStore.ts ← Global (shared across features)
|
|
214
|
+
navigation/types.ts ← Add ProductDetail params
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Implementation (abbreviated — types first):
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 1. types/product.types.ts
|
|
221
|
+
export interface Product {
|
|
222
|
+
id: ProductId;
|
|
223
|
+
title: string;
|
|
224
|
+
price: number;
|
|
225
|
+
description: string;
|
|
226
|
+
images: string[];
|
|
227
|
+
category: string;
|
|
228
|
+
inStock: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface Review {
|
|
232
|
+
id: string;
|
|
233
|
+
userId: UserId;
|
|
234
|
+
rating: number; // 1-5
|
|
235
|
+
comment: string;
|
|
236
|
+
createdAt: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface AddToCartInput {
|
|
240
|
+
productId: ProductId;
|
|
241
|
+
quantity: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. services/productService.ts
|
|
245
|
+
export const productService = {
|
|
246
|
+
getById: (id: ProductId) => api.get<Product>(`/products/${id}`),
|
|
247
|
+
getReviews: (id: ProductId) => api.get<Review[]>(`/products/${id}/reviews`),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// 3. hooks/useProductDetail.ts
|
|
251
|
+
export function useProductDetail(productId: ProductId) {
|
|
252
|
+
const product = useQuery({ queryKey: ['product', productId], queryFn: () => productService.getById(productId) });
|
|
253
|
+
const reviews = useQuery({ queryKey: ['reviews', productId], queryFn: () => productService.getReviews(productId) });
|
|
254
|
+
const addToCart = useCartStore(state => state.addItem);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
product: product.data,
|
|
258
|
+
reviews: reviews.data,
|
|
259
|
+
isLoading: product.isLoading,
|
|
260
|
+
error: product.error,
|
|
261
|
+
refetch: product.refetch,
|
|
262
|
+
handleAddToCart: (quantity: number) => {
|
|
263
|
+
if (!product.data) return;
|
|
264
|
+
addToCart(product.data.id, quantity, product.data.price);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 4-5. Screen composes hook + components with 4 states
|
|
270
|
+
// → Loading: <ProductDetailSkeleton />
|
|
271
|
+
// → Error: <ErrorView onRetry={refetch} />
|
|
272
|
+
// → Empty: <NotFoundView />
|
|
273
|
+
// → Success: <ImageCarousel /> + <ProductInfo /> + <ReviewSummary /> + <AddToCartButton />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Checklist: Verify Against Spec
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
After implementing, check EVERY item from the parsed spec:
|
|
282
|
+
|
|
283
|
+
□ All ENTITIES defined in types?
|
|
284
|
+
□ All FIELDS present in interfaces?
|
|
285
|
+
□ All ACTIONS wired to handlers?
|
|
286
|
+
□ All 4 STATES rendered?
|
|
287
|
+
□ NAVIGATION registered + params typed?
|
|
288
|
+
□ All API endpoints called correctly?
|
|
289
|
+
□ STORAGE persisted where needed?
|
|
290
|
+
□ VALIDATION applied to inputs?
|
|
291
|
+
□ Accessibility labels on interactive elements?
|
|
292
|
+
□ Platform-specific behavior handled (iOS vs Android)?
|
|
293
|
+
```
|