@buivietphi/skill-mobile-mt 2.0.1 → 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.
@@ -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
+ ```
@@ -0,0 +1,312 @@
1
+ # Mobile Storage Patterns
2
+
3
+ > On-device storage — when to use what, how to implement correctly.
4
+ > Covers: AsyncStorage, MMKV, SecureStore/Keychain, SQLite, WatermelonDB, Realm, SharedPreferences.
5
+
6
+ ---
7
+
8
+ ## Decision Matrix — Pick Storage Type First
9
+
10
+ ```
11
+ WHAT ARE YOU STORING? → STORAGE TO USE
12
+ ────────────────────────────────────────────────────────────────────
13
+ Auth tokens / secrets → SecureStore (RN) / Keychain (iOS)
14
+ EncryptedSharedPreferences (Android)
15
+ flutter_secure_storage (Flutter)
16
+
17
+ App preferences / settings → MMKV (RN, fast KV)
18
+ (theme, language, onboarding) SharedPreferences (Android native)
19
+ UserDefaults (iOS native)
20
+ shared_preferences (Flutter)
21
+
22
+ Simple key-value cache → MMKV (RN) / MMKV (Flutter)
23
+ (session data, small objects)
24
+
25
+ Structured relational data → SQLite (via expo-sqlite / sqflite)
26
+ (offline CRUD, complex queries) WatermelonDB (RN, reactive queries)
27
+ drift (Flutter, type-safe)
28
+
29
+ Large offline datasets → WatermelonDB (RN)
30
+ (sync with server, observables) drift (Flutter)
31
+ Room (Android native)
32
+ CoreData / SwiftData (iOS native)
33
+ Realm (cross-platform)
34
+
35
+ Files / images / documents → FileSystem (expo-file-system / path_provider)
36
+ AsyncStorage ❌ (NOT for binary data)
37
+
38
+ ⛔ RULE: AsyncStorage is deprecated for RN. Use MMKV instead.
39
+ ⛔ RULE: NEVER store tokens in AsyncStorage / SharedPreferences / UserDefaults.
40
+ ```
41
+
42
+ ---
43
+
44
+ ## React Native
45
+
46
+ ### 1. Secure Storage (Tokens, Credentials)
47
+
48
+ ```typescript
49
+ // expo-secure-store (Expo) / react-native-keychain (bare RN)
50
+ import * as SecureStore from 'expo-secure-store';
51
+
52
+ // Store
53
+ await SecureStore.setItemAsync('accessToken', token);
54
+
55
+ // Read
56
+ const token = await SecureStore.getItemAsync('accessToken');
57
+
58
+ // Delete (on logout — ALWAYS do this)
59
+ await SecureStore.deleteItemAsync('accessToken');
60
+ await SecureStore.deleteItemAsync('refreshToken');
61
+
62
+ // RULE: On logout, delete ALL secure store keys
63
+ async function logout() {
64
+ await Promise.all([
65
+ SecureStore.deleteItemAsync('accessToken'),
66
+ SecureStore.deleteItemAsync('refreshToken'),
67
+ SecureStore.deleteItemAsync('userId'),
68
+ ]);
69
+ }
70
+ ```
71
+
72
+ ### 2. MMKV (Preferences + KV Cache) — 60x faster than AsyncStorage
73
+
74
+ ```typescript
75
+ // react-native-mmkv
76
+ import { MMKV } from 'react-native-mmkv';
77
+
78
+ // Create instance (one per app, or per domain)
79
+ export const storage = new MMKV();
80
+
81
+ // Typed wrapper (recommended)
82
+ export const Storage = {
83
+ getString: (key: string) => storage.getString(key),
84
+ setString: (key: string, value: string) => storage.set(key, value),
85
+ getBoolean: (key: string) => storage.getBoolean(key) ?? false,
86
+ setBoolean: (key: string, value: boolean) => storage.set(key, value),
87
+ getObject: <T>(key: string): T | null => {
88
+ const raw = storage.getString(key);
89
+ return raw ? JSON.parse(raw) : null;
90
+ },
91
+ setObject: <T>(key: string, value: T) => storage.set(key, JSON.stringify(value)),
92
+ delete: (key: string) => storage.delete(key),
93
+ clear: () => storage.clearAll(),
94
+ };
95
+
96
+ // With Zustand persist (recommended combo)
97
+ import { create } from 'zustand';
98
+ import { persist, createJSONStorage } from 'zustand/middleware';
99
+
100
+ const mmkvStorage = {
101
+ getItem: (name: string) => storage.getString(name) ?? null,
102
+ setItem: (name: string, value: string) => storage.set(name, value),
103
+ removeItem: (name: string) => storage.delete(name),
104
+ };
105
+
106
+ export const useSettingsStore = create(
107
+ persist(
108
+ (set) => ({
109
+ theme: 'light',
110
+ language: 'en',
111
+ setTheme: (theme: string) => set({ theme }),
112
+ setLanguage: (lang: string) => set({ language: lang }),
113
+ }),
114
+ { name: 'settings', storage: createJSONStorage(() => mmkvStorage) }
115
+ )
116
+ );
117
+ ```
118
+
119
+ ### 3. SQLite / WatermelonDB (Structured Offline Data)
120
+
121
+ ```typescript
122
+ // expo-sqlite (simple queries)
123
+ import * as SQLite from 'expo-sqlite';
124
+
125
+ const db = SQLite.openDatabaseSync('app.db');
126
+
127
+ // Init schema
128
+ db.execSync(`
129
+ CREATE TABLE IF NOT EXISTS tasks (
130
+ id TEXT PRIMARY KEY,
131
+ title TEXT NOT NULL,
132
+ completed INTEGER NOT NULL DEFAULT 0,
133
+ created_at INTEGER NOT NULL
134
+ )
135
+ `);
136
+
137
+ // Query
138
+ const tasks = db.getAllSync<Task>('SELECT * FROM tasks WHERE completed = ?', [0]);
139
+
140
+ // Insert
141
+ db.runSync('INSERT INTO tasks (id, title, completed, created_at) VALUES (?, ?, ?, ?)',
142
+ [uuid(), 'Buy milk', 0, Date.now()]);
143
+ ```
144
+
145
+ ```typescript
146
+ // WatermelonDB (reactive queries, sync-ready)
147
+ // Best for: large datasets, reactive UI, server sync
148
+ import { Database } from '@nozbe/watermelondb';
149
+ import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
150
+
151
+ const adapter = new SQLiteAdapter({ schema, migrations });
152
+ const database = new Database({ adapter, modelClasses: [Post, Comment] });
153
+
154
+ // Observe (reactive — auto re-renders on change)
155
+ const posts = database.get('posts').query().observe();
156
+ ```
157
+
158
+ ### 4. Avoid AsyncStorage (Legacy)
159
+
160
+ ```typescript
161
+ // ❌ DEPRECATED — avoid in new projects
162
+ import AsyncStorage from '@react-native-async-storage/async-storage';
163
+
164
+ // ✅ Migrate to MMKV:
165
+ // Before: await AsyncStorage.setItem('theme', 'dark');
166
+ // After: storage.set('theme', 'dark');
167
+
168
+ // ✅ Migrate to expo-secure-store for tokens:
169
+ // Before: await AsyncStorage.setItem('token', jwt);
170
+ // After: await SecureStore.setItemAsync('token', jwt);
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Flutter
176
+
177
+ ### 1. Secure Storage (Tokens)
178
+
179
+ ```dart
180
+ // flutter_secure_storage
181
+ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
182
+
183
+ final _secureStorage = FlutterSecureStorage(
184
+ aOptions: AndroidOptions(encryptedSharedPreferences: true),
185
+ iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
186
+ );
187
+
188
+ // Store
189
+ await _secureStorage.write(key: 'accessToken', value: token);
190
+
191
+ // Read
192
+ final token = await _secureStorage.read(key: 'accessToken');
193
+
194
+ // Delete on logout
195
+ await _secureStorage.deleteAll();
196
+ ```
197
+
198
+ ### 2. SharedPreferences / Hive (KV Storage)
199
+
200
+ ```dart
201
+ // shared_preferences (simple, built-in)
202
+ final prefs = await SharedPreferences.getInstance();
203
+ await prefs.setString('language', 'en');
204
+ final lang = prefs.getString('language') ?? 'en';
205
+
206
+ // Hive (faster, type-safe, no codegen)
207
+ import 'package:hive_flutter/hive_flutter.dart';
208
+
209
+ await Hive.initFlutter();
210
+ final box = await Hive.openBox('settings');
211
+ box.put('theme', 'dark');
212
+ final theme = box.get('theme', defaultValue: 'light');
213
+ ```
214
+
215
+ ### 3. Drift (SQLite, type-safe)
216
+
217
+ ```dart
218
+ // drift — type-safe SQLite with code generation
219
+ @DriftDatabase(tables: [Tasks])
220
+ class AppDatabase extends _$AppDatabase {
221
+ AppDatabase() : super(_openConnection());
222
+
223
+ Stream<List<Task>> watchIncompleteTasks() =>
224
+ (select(tasks)..where((t) => t.completed.not())).watch();
225
+
226
+ Future insertTask(TasksCompanion task) => into(tasks).insert(task);
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## iOS Native (Swift)
233
+
234
+ ```swift
235
+ // UserDefaults — preferences ONLY (not tokens)
236
+ UserDefaults.standard.set("en", forKey: "language")
237
+ let lang = UserDefaults.standard.string(forKey: "language") ?? "en"
238
+
239
+ // Keychain — tokens and secrets
240
+ import Security
241
+
242
+ func saveToKeychain(key: String, value: String) {
243
+ let data = value.data(using: .utf8)!
244
+ let query: [String: Any] = [
245
+ kSecClass as String: kSecClassGenericPassword,
246
+ kSecAttrAccount as String: key,
247
+ kSecValueData as String: data,
248
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
249
+ ]
250
+ SecItemDelete(query as CFDictionary)
251
+ SecItemAdd(query as CFDictionary, nil)
252
+ }
253
+
254
+ // SwiftData / CoreData — structured offline data
255
+ @Model class Task {
256
+ var id: UUID
257
+ var title: String
258
+ var completed: Bool
259
+ init(title: String) { self.id = UUID(); self.title = title; self.completed = false }
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Android Native (Kotlin)
266
+
267
+ ```kotlin
268
+ // EncryptedSharedPreferences — tokens and secrets
269
+ val masterKey = MasterKey.Builder(context)
270
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
271
+ val encryptedPrefs = EncryptedSharedPreferences.create(
272
+ context, "secure_prefs", masterKey,
273
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
274
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
275
+ )
276
+ encryptedPrefs.edit().putString("accessToken", token).apply()
277
+
278
+ // DataStore — preferences (replaces SharedPreferences)
279
+ val Context.dataStore by preferencesDataStore(name = "settings")
280
+ val LANGUAGE_KEY = stringPreferencesKey("language")
281
+
282
+ suspend fun saveLanguage(context: Context, lang: String) {
283
+ context.dataStore.edit { it[LANGUAGE_KEY] = lang }
284
+ }
285
+
286
+ val languageFlow = context.dataStore.data.map { it[LANGUAGE_KEY] ?: "en" }
287
+
288
+ // Room — structured offline data
289
+ @Entity data class Task(@PrimaryKey val id: String, val title: String, val completed: Boolean)
290
+ @Dao interface TaskDao {
291
+ @Query("SELECT * FROM task WHERE completed = 0") fun getActive(): Flow<List<Task>>
292
+ @Insert suspend fun insert(task: Task)
293
+ }
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Security Checklist
299
+
300
+ ```
301
+ ✅ Tokens → SecureStore / Keychain / EncryptedSharedPreferences ONLY
302
+ ✅ On logout → delete ALL secure storage keys
303
+ ✅ Encrypt sensitive data before storing in SQLite/MMKV
304
+ ✅ Don't log stored values (console.log, print)
305
+ ✅ Use device-only accessibility (not iCloud sync for tokens)
306
+
307
+ ⛔ NEVER: AsyncStorage for tokens
308
+ ⛔ NEVER: UserDefaults for tokens
309
+ ⛔ NEVER: SharedPreferences (unencrypted) for tokens
310
+ ⛔ NEVER: Log token values in debug output
311
+ ⛔ NEVER: Store plain-text passwords
312
+ ```