@buivietphi/skill-mobile-mt 2.1.0 → 2.2.1

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.
@@ -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
+ ```