@buivietphi/skill-mobile-mt 1.3.0 → 1.4.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.
Potentially problematic release.
This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.
- package/AGENTS.md +19 -17
- package/README.md +75 -13
- package/SKILL.md +14 -3
- package/android/android-native.md +50 -0
- package/bin/install.mjs +41 -14
- package/flutter/flutter.md +58 -0
- package/humanizer/humanizer-mobile.md +295 -0
- package/package.json +3 -2
- package/shared/offline-first.md +377 -0
- package/shared/platform-excellence.md +85 -0
package/AGENTS.md
CHANGED
|
@@ -34,13 +34,13 @@ skill-mobile-mt/
|
|
|
34
34
|
│ └── react-native.md ← RN + Expo patterns (5,108 tokens)
|
|
35
35
|
│
|
|
36
36
|
├── flutter/
|
|
37
|
-
│ └── flutter.md ← Flutter + Dart patterns (
|
|
37
|
+
│ └── flutter.md ← Flutter + Dart 3.x patterns (2,100 tokens)
|
|
38
38
|
│
|
|
39
39
|
├── ios/
|
|
40
40
|
│ └── ios-native.md ← Swift + UIKit/SwiftUI patterns (1,452 tokens)
|
|
41
41
|
│
|
|
42
42
|
├── android/
|
|
43
|
-
│ └── android-native.md ← Kotlin + Compose patterns (
|
|
43
|
+
│ └── android-native.md ← Kotlin + Compose patterns (2,100 tokens)
|
|
44
44
|
│
|
|
45
45
|
└── shared/
|
|
46
46
|
│
|
|
@@ -54,21 +54,23 @@ skill-mobile-mt/
|
|
|
54
54
|
├── document-analysis.md ← Parse images/PDFs/DOCX → code (1,200 tokens)
|
|
55
55
|
├── anti-patterns.md ← PII, cardinality, payload detection (2,800 tokens)
|
|
56
56
|
├── performance-prediction.md ← Frame budget, FPS prediction (1,500 tokens)
|
|
57
|
-
├── platform-excellence.md ← iOS 18+ vs Android 15+ UX (
|
|
57
|
+
├── platform-excellence.md ← iOS 18+ vs Android 15+ UX + HIG (2,200 tokens)
|
|
58
58
|
├── version-management.md ← SDK compat matrix + release testing (3,500 tokens)
|
|
59
59
|
├── observability.md ← Sessions as 4th pillar (3,000 tokens)
|
|
60
60
|
├── architecture-intelligence.md ← Patterns from 30+ production repos (4,500 tokens)
|
|
61
61
|
├── common-pitfalls.md ← Known issue patterns (1,160 tokens)
|
|
62
62
|
├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
|
|
63
63
|
│
|
|
64
|
+
├── offline-first.md ← Local-first + sync patterns (2,566 tokens)
|
|
65
|
+
│
|
|
64
66
|
├── ── TEMPLATES (copy to your project) ────────────────────
|
|
65
67
|
├── claude-md-template.md ← CLAUDE.md for Claude Code (copy to project root)
|
|
66
68
|
└── agent-rules-template.md ← Rules for ALL agents: Cursor/.cursorrules, Windsurf/.windsurfrules, Copilot/.github/copilot-instructions.md, Codex/AGENTS.md, Gemini/GEMINI.md, Antigravity YAML
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
**Token totals:**
|
|
70
|
-
- Smart load (1 platform + core shared): **~
|
|
71
|
-
- Full load (all files): **~
|
|
72
|
+
- Smart load (1 platform + core shared): **~38,600 tokens** (30.2% of 128K)
|
|
73
|
+
- Full load (all files): **~70,000 tokens** (54.7% of 128K)
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
@@ -80,13 +82,13 @@ skill-mobile-mt/
|
|
|
80
82
|
|
|
81
83
|
**Loads automatically:**
|
|
82
84
|
```
|
|
83
|
-
SKILL.md (
|
|
84
|
-
+ 1 platform file (~1,
|
|
85
|
-
+ shared/code-review.md (
|
|
86
|
-
+ shared/bug-detection.md (
|
|
87
|
-
+ shared/prompt-engineering.md (
|
|
85
|
+
SKILL.md (~13,200 tokens)
|
|
86
|
+
+ 1 platform file (~1,580–5,730 tokens depending on platform)
|
|
87
|
+
+ shared/code-review.md (~1,500 tokens)
|
|
88
|
+
+ shared/bug-detection.md (~800 tokens)
|
|
89
|
+
+ shared/prompt-engineering.md (~5,600 tokens)
|
|
88
90
|
─────────────────────────────────────────────────
|
|
89
|
-
≈
|
|
91
|
+
≈ 38,600 tokens total (estimated)
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
**Use case:** Regular coding, new features, code review. Covers 90% of daily work.
|
|
@@ -116,7 +118,7 @@ The agent reads the task, then decides which extra file to load:
|
|
|
116
118
|
|
|
117
119
|
**No automatic trigger.** Full load happens when the AI reads every file without being selective — either because it's over-eager, or because the user explicitly asks for it.
|
|
118
120
|
|
|
119
|
-
**Total:** ~
|
|
121
|
+
**Total:** ~70,000 tokens (54.7% of 128K, 35% of 200K)
|
|
120
122
|
|
|
121
123
|
**How it actually works:**
|
|
122
124
|
- `@skill-mobile-mt` only injects SKILL.md into context
|
|
@@ -141,7 +143,7 @@ The agent reads the task, then decides which extra file to load:
|
|
|
141
143
|
```yaml
|
|
142
144
|
skill:
|
|
143
145
|
name: skill-mobile-mt
|
|
144
|
-
version: "1.
|
|
146
|
+
version: "1.4.0"
|
|
145
147
|
author: buivietphi
|
|
146
148
|
category: engineering
|
|
147
149
|
tags:
|
|
@@ -208,9 +210,9 @@ skill:
|
|
|
208
210
|
java: ".java files in app/src/"
|
|
209
211
|
|
|
210
212
|
context_budget:
|
|
211
|
-
max_tokens:
|
|
212
|
-
smart_load_tokens:
|
|
213
|
-
savings: "~
|
|
213
|
+
max_tokens: 70000
|
|
214
|
+
smart_load_tokens: 38600
|
|
215
|
+
savings: "~45%"
|
|
214
216
|
```
|
|
215
217
|
|
|
216
218
|
---
|
|
@@ -455,7 +457,7 @@ npx @buivietphi/skill-mobile-mt --init all # → all files
|
|
|
455
457
|
{
|
|
456
458
|
"id": "skill-mobile-mt",
|
|
457
459
|
"name": "skill-mobile-mt",
|
|
458
|
-
"version": "1.
|
|
460
|
+
"version": "1.4.0",
|
|
459
461
|
"author": "buivietphi",
|
|
460
462
|
"category": "engineering",
|
|
461
463
|
"description": "Master Senior Mobile Engineer. Pre-built patterns from 18 production apps + project adaptation. Auto-detects language and framework. React Native, Flutter, iOS, Android.",
|
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ The skill automatically detects before any action:
|
|
|
209
209
|
|
|
210
210
|
## Smart Loading
|
|
211
211
|
|
|
212
|
-
Only loads the relevant platform docs — saves **~
|
|
212
|
+
Only loads the relevant platform docs — saves **~45% context tokens** vs loading everything.
|
|
213
213
|
|
|
214
214
|
```
|
|
215
215
|
Flutter project?
|
|
@@ -229,22 +229,22 @@ iOS only?
|
|
|
229
229
|
|
|
230
230
|
| Scenario | Tokens | % of 128K | % of 200K |
|
|
231
231
|
|----------|-------:|----------:|----------:|
|
|
232
|
-
| SKILL.md only | ~
|
|
233
|
-
| + 1 platform + core shared/ | ~
|
|
234
|
-
| Cross-platform (RN/Flutter + iOS + Android) | ~
|
|
235
|
-
| All files loaded | ~
|
|
236
|
-
| **Smart load (recommended)** | **~
|
|
232
|
+
| SKILL.md only | ~13,200 | 10.3% | 6.6% |
|
|
233
|
+
| + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
|
|
234
|
+
| Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
|
|
235
|
+
| All files loaded | ~70,000 | 54.7% | 35.0% |
|
|
236
|
+
| **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
|
|
237
237
|
|
|
238
238
|
### Per-file token breakdown
|
|
239
239
|
|
|
240
240
|
| File | Tokens |
|
|
241
241
|
|------|-------:|
|
|
242
242
|
| `SKILL.md` | 6,100 |
|
|
243
|
-
| `AGENTS.md` | 1,
|
|
243
|
+
| `AGENTS.md` | 1,600 |
|
|
244
244
|
| `react-native/react-native.md` | 5,108 |
|
|
245
|
-
| `flutter/flutter.md` |
|
|
245
|
+
| `flutter/flutter.md` | 2,100 |
|
|
246
246
|
| `ios/ios-native.md` | 1,452 |
|
|
247
|
-
| `android/android-native.md` |
|
|
247
|
+
| `android/android-native.md` | 2,100 |
|
|
248
248
|
| `shared/code-review.md` | 865 |
|
|
249
249
|
| `shared/bug-detection.md` | 499 |
|
|
250
250
|
| `shared/prompt-engineering.md` | 3,927 |
|
|
@@ -255,12 +255,13 @@ iOS only?
|
|
|
255
255
|
| `shared/release-checklist.md` | 587 |
|
|
256
256
|
| `shared/anti-patterns.md` | 2,800 |
|
|
257
257
|
| `shared/performance-prediction.md` | 1,500 |
|
|
258
|
-
| `shared/platform-excellence.md` |
|
|
258
|
+
| `shared/platform-excellence.md` | 2,200 |
|
|
259
259
|
| `shared/version-management.md` | 3,500 |
|
|
260
260
|
| `shared/observability.md` | 3,000 |
|
|
261
|
+
| `shared/offline-first.md` | 2,566 |
|
|
261
262
|
| `shared/claude-md-template.md` | ~500 |
|
|
262
263
|
| `shared/agent-rules-template.md` | ~2,500 |
|
|
263
|
-
| **Total** | **~
|
|
264
|
+
| **Total** | **~48,800** |
|
|
264
265
|
|
|
265
266
|
## Installed Structure
|
|
266
267
|
|
|
@@ -287,10 +288,11 @@ iOS only?
|
|
|
287
288
|
├── document-analysis.md Parse docs/images → code
|
|
288
289
|
├── anti-patterns.md PII, cardinality, payload detection
|
|
289
290
|
├── performance-prediction.md Frame budget, FPS prediction
|
|
290
|
-
├── platform-excellence.md iOS 18+ vs Android 15+ guidelines
|
|
291
|
+
├── platform-excellence.md iOS 18+ vs Android 15+ guidelines + HIG
|
|
291
292
|
├── version-management.md SDK compatibility matrix
|
|
292
293
|
├── observability.md Sessions as 4th pillar
|
|
293
294
|
├── release-checklist.md Pre-release verification
|
|
295
|
+
├── offline-first.md Local-first + sync patterns
|
|
294
296
|
├── claude-md-template.md CLAUDE.md template for projects
|
|
295
297
|
└── agent-rules-template.md Rules templates for all agents
|
|
296
298
|
```
|
|
@@ -385,7 +387,7 @@ your-project/
|
|
|
385
387
|
|
|
386
388
|
- **Anti-Pattern Detection** (`anti-patterns.md`): Detect PII leaks (CRITICAL), high cardinality tags, unbounded payloads, unstructured logs, sync telemetry on main thread — with auto-fix suggestions
|
|
387
389
|
- **Performance Prediction** (`performance-prediction.md`): Calculate frame budget, FlatList bridge calls, and memory usage BEFORE writing code. Example: `50 items × 3 bridge calls × 0.3ms = 45ms/frame → 22 FPS ❌ JANK`
|
|
388
|
-
- **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography,
|
|
390
|
+
- **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography, haptic feedback types, permission timing, ratings prompt flow, Live Activities/Dynamic Island, performance targets (cold start < 1s iOS, < 1.5s Android)
|
|
389
391
|
- **Version Management** (`version-management.md`): Full SDK compatibility matrix for RN 0.73-0.76, Expo 50-52, Flutter 3.22-3.27, iOS 16-18, Android 13-15. Check SDK compat BEFORE `npm install`. Release-mode testing protocol.
|
|
390
392
|
- **Observability** (`observability.md`): Sessions as the 4th pillar (Metrics + Logs + Traces + **Sessions**). Session lifecycle, enrichment API, unified instrumentation stack, correlation queries. Every event carries `session_id` for full user journey reconstruction.
|
|
391
393
|
|
|
@@ -421,6 +423,66 @@ your-project/
|
|
|
421
423
|
| **Kiro** | Reads `.kiro/steering/` from project root | `npx skill-mobile-mt --init kiro` |
|
|
422
424
|
| **Antigravity** | Reads from `~/.agents/skills/` | `npx skill-mobile-mt --antigravity` |
|
|
423
425
|
|
|
426
|
+
## Humanizer — Mobile Copy
|
|
427
|
+
|
|
428
|
+
Installed automatically alongside the main skill. Removes AI writing patterns from mobile app text.
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
@humanizer-mobile
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Use for: app store descriptions, release notes, error messages, onboarding, push notifications, permission prompts, paywall copy.
|
|
435
|
+
|
|
436
|
+
### Examples
|
|
437
|
+
|
|
438
|
+
**App Store Description**
|
|
439
|
+
```
|
|
440
|
+
❌ "TaskFlow is a powerful, comprehensive task management application that
|
|
441
|
+
seamlessly integrates with your existing workflow."
|
|
442
|
+
|
|
443
|
+
✅ "TaskFlow keeps your team's work in one place. Add tasks, assign them,
|
|
444
|
+
set deadlines — everything syncs in real time. Works with Slack and Notion."
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Release Notes**
|
|
448
|
+
```
|
|
449
|
+
❌ "Version 2.1.0 introduces significant enhancements to the overall user
|
|
450
|
+
experience including robust improvements to performance and reliability."
|
|
451
|
+
|
|
452
|
+
✅ "2.1.0
|
|
453
|
+
- Faster load times on older Android devices (was 4s, now 1.2s)
|
|
454
|
+
- Fixed crash when opening notifications while offline
|
|
455
|
+
- Dark mode now remembers your setting between sessions"
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Error Messages**
|
|
459
|
+
```
|
|
460
|
+
❌ "We apologize for the inconvenience. An unexpected error has occurred."
|
|
461
|
+
|
|
462
|
+
✅ "Couldn't save — no internet connection.
|
|
463
|
+
[Try again] [Save for later]"
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Push Notifications**
|
|
467
|
+
```
|
|
468
|
+
❌ "You have received a new message from a team member."
|
|
469
|
+
✅ "Alex commented on "Landing page redesign""
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Paywall**
|
|
473
|
+
```
|
|
474
|
+
❌ "Unlock the full potential of our comprehensive premium features."
|
|
475
|
+
|
|
476
|
+
✅ "Go Pro
|
|
477
|
+
• Unlimited projects (free plan: 3)
|
|
478
|
+
• Team sharing
|
|
479
|
+
[Start 7-day free trial]"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Covers 24 AI patterns across: content inflation, chatbot artifacts, style issues, mobile-specific copy (App Store limits, permission prompts, rating requests, subscription CTAs).
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
424
486
|
## License
|
|
425
487
|
|
|
426
488
|
MIT — by [buivietphi](https://github.com/buivietphi)
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-mobile-mt
|
|
3
3
|
description: "Master Senior Mobile Engineer. Patterns from 30+ production repos (200k+ GitHub stars: Ignite, Expensify, Mattermost, Immich, AppFlowy, Now in Android, TCA). Use when: building mobile features, fixing mobile bugs, reviewing mobile code, mobile architecture, React Native, Flutter, iOS Swift, Android Kotlin, mobile performance, mobile security audit, mobile code review, app release. Two modes: (1) default = pre-built production patterns, (2) 'project' = reads current project and adapts."
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.4.1"
|
|
5
5
|
author: buivietphi
|
|
6
6
|
priority: high
|
|
7
7
|
user-invocable: true
|
|
@@ -126,6 +126,10 @@ USER REQUEST → ACTION (Read tool required)
|
|
|
126
126
|
|
|
127
127
|
"Build error / runtime crash" → Read: shared/error-recovery.md
|
|
128
128
|
then: apply matching fix pattern
|
|
129
|
+
|
|
130
|
+
"Offline / cache / sync" → Read: shared/offline-first.md
|
|
131
|
+
then: implement local-first architecture
|
|
132
|
+
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
**⛔ NEVER start coding without identifying the task type first.**
|
|
@@ -1358,9 +1362,14 @@ skill-mobile-mt/
|
|
|
1358
1362
|
├── ios/ios-native.md ← iOS Swift MVVM + Clean Architecture
|
|
1359
1363
|
├── android/android-native.md ← Android Kotlin + Clean Architecture
|
|
1360
1364
|
└── shared/
|
|
1365
|
+
│
|
|
1366
|
+
├── ── CORE (always load) ────────────────────────────────
|
|
1361
1367
|
├── code-review.md ← 🔴 Senior review checklist
|
|
1362
1368
|
├── bug-detection.md ← 🔴 Auto bug scanner
|
|
1363
|
-
├── prompt-engineering.md ←
|
|
1369
|
+
├── prompt-engineering.md ← 🔴 Auto-think templates
|
|
1370
|
+
│
|
|
1371
|
+
├── ── ON-DEMAND (load by task) ──────────────────────────
|
|
1372
|
+
├── architecture-intelligence.md ← 🟡 Patterns from 30+ production repos
|
|
1364
1373
|
├── release-checklist.md ← 🟡 Before shipping to app store
|
|
1365
1374
|
├── common-pitfalls.md ← 🟡 Problem → Symptoms → Solution
|
|
1366
1375
|
├── error-recovery.md ← 🟡 Fix build/runtime errors
|
|
@@ -1369,5 +1378,7 @@ skill-mobile-mt/
|
|
|
1369
1378
|
├── performance-prediction.md ← 🟡 Predict FPS/memory BEFORE shipping
|
|
1370
1379
|
├── platform-excellence.md ← 🟡 iOS 18+ vs Android 15+ guidelines
|
|
1371
1380
|
├── version-management.md ← 🟡 SDK compatibility matrix
|
|
1372
|
-
|
|
1381
|
+
├── observability.md ← 🟡 Sessions as 4th pillar
|
|
1382
|
+
│
|
|
1383
|
+
└── offline-first.md ← 🟢 Local-first + sync patterns
|
|
1373
1384
|
```
|
|
@@ -192,6 +192,54 @@ dependencies {
|
|
|
192
192
|
}
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## Compose Performance Optimization
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// @Stable / @Immutable — tell Compose when to skip recomposition
|
|
199
|
+
// Use when your class isn't a data class but values never change
|
|
200
|
+
@Stable
|
|
201
|
+
class UserState(val id: String, val name: String)
|
|
202
|
+
|
|
203
|
+
@Immutable
|
|
204
|
+
data class ProductUiModel(val id: String, val price: Double)
|
|
205
|
+
|
|
206
|
+
// derivedStateOf — compute derived state only when inputs change
|
|
207
|
+
// Prevents recomposition on every scroll position change
|
|
208
|
+
val showFab by remember {
|
|
209
|
+
derivedStateOf { listState.firstVisibleItemIndex > 0 }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// key() in LazyColumn — stable identity prevents full recomposition
|
|
213
|
+
LazyColumn {
|
|
214
|
+
items(products, key = { it.id }) { product ->
|
|
215
|
+
ProductCard(product) // Only recomposes if THIS product changes
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Stateless components — pass data + callbacks, not ViewModel
|
|
220
|
+
@Composable
|
|
221
|
+
fun ProductCard(
|
|
222
|
+
product: Product, // data only
|
|
223
|
+
onClick: () -> Unit, // callback only
|
|
224
|
+
) { /* no ViewModel here */ }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Baseline Profiles (Startup Optimization)
|
|
228
|
+
|
|
229
|
+
```kotlin
|
|
230
|
+
// app/src/main/baseline-prof.txt (generated by Macrobenchmark)
|
|
231
|
+
// Speeds up cold start 20-30% by AOT-compiling hot code paths
|
|
232
|
+
|
|
233
|
+
// build.gradle.kts
|
|
234
|
+
dependencies {
|
|
235
|
+
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate with Macrobenchmark:
|
|
239
|
+
// ./gradlew :app:generateBaselineProfile
|
|
240
|
+
// Commit the generated baseline-prof.txt
|
|
241
|
+
```
|
|
242
|
+
|
|
195
243
|
## Common Pitfalls
|
|
196
244
|
|
|
197
245
|
| Pitfall | Fix |
|
|
@@ -201,6 +249,8 @@ dependencies {
|
|
|
201
249
|
| Context leak | `@ApplicationContext`, never Activity |
|
|
202
250
|
| Missing ProGuard | Test release builds |
|
|
203
251
|
| Main thread blocking | `Dispatchers.IO` |
|
|
252
|
+
| Unstable lambdas in Compose | `remember { {} }` or move to ViewModel |
|
|
253
|
+
| List without keys | `items(list, key = { it.id })` |
|
|
204
254
|
|
|
205
255
|
---
|
|
206
256
|
|
package/bin/install.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* npx @buivietphi/skill-mobile --init all # Generate all project-level files
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
29
29
|
import { join, resolve, dirname } from 'node:path';
|
|
30
30
|
import { homedir } from 'node:os';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
@@ -38,13 +38,14 @@ const HOME = homedir();
|
|
|
38
38
|
// Structure: root files + subfolders
|
|
39
39
|
const ROOT_FILES = ['SKILL.md', 'AGENTS.md'];
|
|
40
40
|
|
|
41
|
-
const SUBFOLDERS =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const SUBFOLDERS = ['react-native', 'flutter', 'ios', 'android', 'shared'];
|
|
42
|
+
|
|
43
|
+
// Read all .md files from a folder dynamically
|
|
44
|
+
function getMdFiles(folder) {
|
|
45
|
+
const dir = join(PKG_ROOT, folder);
|
|
46
|
+
if (!existsSync(dir)) return [];
|
|
47
|
+
return readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
48
|
+
}
|
|
48
49
|
|
|
49
50
|
const AGENTS = {
|
|
50
51
|
claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
|
|
@@ -69,7 +70,7 @@ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
|
|
|
69
70
|
|
|
70
71
|
function banner() {
|
|
71
72
|
log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
|
|
72
|
-
log(` │ 📱 @buivietphi/skill-mobile-mt v1.
|
|
73
|
+
log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.1 │`);
|
|
73
74
|
log(` │ Master Senior Mobile Engineer │`);
|
|
74
75
|
log(` │ │`);
|
|
75
76
|
log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
|
|
@@ -92,9 +93,9 @@ function showContext() {
|
|
|
92
93
|
total += t;
|
|
93
94
|
log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
|
|
94
95
|
}
|
|
95
|
-
for (const
|
|
96
|
+
for (const folder of SUBFOLDERS) {
|
|
96
97
|
let ft = 0;
|
|
97
|
-
for (const f of
|
|
98
|
+
for (const f of getMdFiles(folder)) ft += tokenCount(join(PKG_ROOT, folder, f));
|
|
98
99
|
total += ft;
|
|
99
100
|
log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${ft.toLocaleString()} tokens${c.reset}`);
|
|
100
101
|
}
|
|
@@ -104,6 +105,7 @@ function showContext() {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function install(baseDir, agentName) {
|
|
108
|
+
// Install main skill
|
|
107
109
|
const dst = join(baseDir, SKILL_NAME);
|
|
108
110
|
mkdirSync(dst, { recursive: true });
|
|
109
111
|
let n = 0;
|
|
@@ -113,17 +115,26 @@ function install(baseDir, agentName) {
|
|
|
113
115
|
cpSync(src, join(dst, f), { force: true });
|
|
114
116
|
n++;
|
|
115
117
|
}
|
|
116
|
-
for (const
|
|
118
|
+
for (const folder of SUBFOLDERS) {
|
|
117
119
|
const dstFolder = join(dst, folder);
|
|
118
120
|
mkdirSync(dstFolder, { recursive: true });
|
|
119
|
-
for (const f of
|
|
121
|
+
for (const f of getMdFiles(folder)) {
|
|
120
122
|
const src = join(PKG_ROOT, folder, f);
|
|
121
|
-
if (!existsSync(src)) continue;
|
|
122
123
|
cpSync(src, join(dstFolder, f), { force: true });
|
|
123
124
|
n++;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
|
|
128
|
+
|
|
129
|
+
// Auto-install humanizer-mobile as separate skill
|
|
130
|
+
const humanizerSrc = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
|
|
131
|
+
if (existsSync(humanizerSrc)) {
|
|
132
|
+
const humDst = join(baseDir, 'humanizer-mobile');
|
|
133
|
+
mkdirSync(humDst, { recursive: true });
|
|
134
|
+
cpSync(humanizerSrc, join(humDst, 'humanizer-mobile.md'), { force: true });
|
|
135
|
+
ok(`${c.bold}humanizer-mobile/${c.reset} → ${agentName} ${c.dim}(${humDst})${c.reset}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
return n;
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -895,6 +906,22 @@ async function main() {
|
|
|
895
906
|
} else if (flags.has('auto')) {
|
|
896
907
|
targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
897
908
|
if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
|
|
909
|
+
} else if (flags.has('humanizer')) {
|
|
910
|
+
// Install humanizer-mobile as a separate skill
|
|
911
|
+
const src = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
|
|
912
|
+
if (!existsSync(src)) { fail('humanizer/humanizer-mobile.md not found'); process.exit(1); }
|
|
913
|
+
const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
|
|
914
|
+
const agentKeys = detected.length ? detected : ['claude'];
|
|
915
|
+
for (const k of agentKeys) {
|
|
916
|
+
const dst = join(AGENTS[k].dir, 'humanizer-mobile');
|
|
917
|
+
mkdirSync(dst, { recursive: true });
|
|
918
|
+
cpSync(src, join(dst, 'humanizer-mobile.md'), { force: true });
|
|
919
|
+
ok(`${c.bold}humanizer-mobile/${c.reset} → ${AGENTS[k].name} ${c.dim}(${dst})${c.reset}`);
|
|
920
|
+
}
|
|
921
|
+
log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
|
|
922
|
+
log(` ${c.bold}Usage:${c.reset}`);
|
|
923
|
+
log(` ${c.cyan}@humanizer-mobile${c.reset} Humanize app store copy, release notes, error messages\n`);
|
|
924
|
+
return;
|
|
898
925
|
} else if (flags.has('path')) {
|
|
899
926
|
const p = args[args.indexOf('--path') + 1];
|
|
900
927
|
if (!p) { fail('--path needs a directory'); process.exit(1); }
|
package/flutter/flutter.md
CHANGED
|
@@ -240,7 +240,65 @@ FirebaseMessaging.onMessage.listen((message) {
|
|
|
240
240
|
| Firebase | firebase_core, messaging, firestore |
|
|
241
241
|
| Image | cached_network_image |
|
|
242
242
|
|
|
243
|
+
## Dart 3.x Patterns
|
|
244
|
+
|
|
245
|
+
```dart
|
|
246
|
+
// Sealed classes — exhaustive pattern matching (replaces abstract + subclasses)
|
|
247
|
+
sealed class AuthState {}
|
|
248
|
+
class Authenticated extends AuthState { final User user; Authenticated(this.user); }
|
|
249
|
+
class Unauthenticated extends AuthState {}
|
|
250
|
+
class Loading extends AuthState {}
|
|
251
|
+
|
|
252
|
+
// Usage — compiler enforces all cases are handled
|
|
253
|
+
Widget build() => switch (authState) {
|
|
254
|
+
Authenticated(:final user) => HomeScreen(user: user),
|
|
255
|
+
Unauthenticated() => LoginScreen(),
|
|
256
|
+
Loading() => const CircularProgressIndicator(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Records — lightweight tuples with named fields (Dart 3.0)
|
|
260
|
+
(String name, int age) getUser() => ('Alice', 30);
|
|
261
|
+
final (name, age) = getUser();
|
|
262
|
+
|
|
263
|
+
// Pattern matching in if/switch
|
|
264
|
+
if (response case {'status': 'ok', 'data': final data}) {
|
|
265
|
+
processData(data);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Riverpod + sealed class = clean state
|
|
269
|
+
@riverpod
|
|
270
|
+
class AuthNotifier extends _$AuthNotifier {
|
|
271
|
+
@override
|
|
272
|
+
AuthState build() => Unauthenticated();
|
|
273
|
+
|
|
274
|
+
Future<void> login(String email, String password) async {
|
|
275
|
+
state = Loading();
|
|
276
|
+
state = switch (await _repo.login(email, password)) {
|
|
277
|
+
Ok(:final value) => Authenticated(value),
|
|
278
|
+
Err(:final error) => Unauthenticated(), // or a separate Error state
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Impeller (Flutter 3.10+)
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
Impeller is Flutter's new rendering engine — enabled by default on iOS.
|
|
288
|
+
Android: opt-in via AndroidManifest.xml
|
|
289
|
+
|
|
290
|
+
Benefits:
|
|
291
|
+
- Eliminates shader compilation jank (the "jank on first frame" problem)
|
|
292
|
+
- Consistent 60/120fps even on first run
|
|
293
|
+
|
|
294
|
+
Enable on Android:
|
|
295
|
+
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="true" />
|
|
296
|
+
|
|
297
|
+
Test: flutter run --enable-impeller
|
|
298
|
+
```
|
|
299
|
+
|
|
243
300
|
---
|
|
244
301
|
|
|
245
302
|
> Standard: Riverpod + get_it/injectable + Clean Architecture.
|
|
246
303
|
> Dio/Retrofit for complex APIs. Floor for offline-first. Firebase for push/analytics.
|
|
304
|
+
> Dart 3.x: sealed classes + records for type-safe state.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Humanizer — Mobile Copy & Text
|
|
2
|
+
|
|
3
|
+
> Invoke with: @humanizer-mobile
|
|
4
|
+
> Use for: app store descriptions, release notes, error messages, onboarding copy, push notifications, UI labels
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What this does
|
|
9
|
+
|
|
10
|
+
Removes AI-generated writing patterns from mobile app text. Makes copy sound like a real person wrote it — not a language model.
|
|
11
|
+
|
|
12
|
+
**Rule:** Sterile, voiceless writing is just as obvious as slop.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The 24 AI Patterns to Kill
|
|
17
|
+
|
|
18
|
+
### Content
|
|
19
|
+
| Pattern | Example | Fix |
|
|
20
|
+
|---------|---------|-----|
|
|
21
|
+
| Inflated significance | "This significantly enhances the overall user experience" | "Checkout is now 3 steps instead of 7" |
|
|
22
|
+
| Notability inflation | "A powerful, robust, feature-rich solution" | Just describe what it does |
|
|
23
|
+
| Promotional language | "Seamlessly integrates with your workflow" | "Connects to Slack and Notion" |
|
|
24
|
+
| Superficial -ing analysis | "By leveraging cutting-edge technology..." | State what the tech actually does |
|
|
25
|
+
|
|
26
|
+
### Language
|
|
27
|
+
| Pattern | Example | Fix |
|
|
28
|
+
|---------|---------|-----|
|
|
29
|
+
| Copula avoidance | "The app, being designed for..." | "The app works for..." |
|
|
30
|
+
| Negative parallelism | "Not only fast, but also reliable, and furthermore secure" | Pick the one that matters most |
|
|
31
|
+
| Rule of three | "Simple, powerful, and intuitive" | Say which one is actually true |
|
|
32
|
+
| Elegant variation | "application... software... platform... tool..." | Pick one word and use it |
|
|
33
|
+
|
|
34
|
+
### Style
|
|
35
|
+
| Pattern | Example | Fix |
|
|
36
|
+
|---------|---------|-----|
|
|
37
|
+
| Em dash overuse | "Our app — designed for professionals — helps you..." | Rewrite the sentence |
|
|
38
|
+
| Unnecessary bold | "This **feature** allows **users** to **manage** their **tasks**" | Bold nothing or bold the one key thing |
|
|
39
|
+
| Inline headers mid-paragraph | "**Key features:** The app includes..." | Just write the features |
|
|
40
|
+
| Emoji decoration | "🚀 Fast • 💪 Powerful • ✨ Beautiful" | Use 0 or 1 emoji max, never as decoration |
|
|
41
|
+
|
|
42
|
+
### Communication (chatbot artifacts)
|
|
43
|
+
| Pattern | Example | Fix |
|
|
44
|
+
|---------|---------|-----|
|
|
45
|
+
| "I hope this helps" | Any form of it | Delete |
|
|
46
|
+
| "Feel free to..." | "Feel free to reach out" | "Contact us" |
|
|
47
|
+
| "Please note that" | "Please note that this feature requires..." | "This feature requires..." |
|
|
48
|
+
| Knowledge cutoff disclaimer | "As of my last update..." | Never use in app copy |
|
|
49
|
+
| "Delve into" | "Delve into your analytics" | "Check your analytics" |
|
|
50
|
+
| "Leverage" | "Leverage our AI" | "Use our AI" |
|
|
51
|
+
| "Streamline" | "Streamline your workflow" | Say what actually gets faster |
|
|
52
|
+
| "Robust" | "A robust solution" | Say what makes it strong |
|
|
53
|
+
| "Seamlessly" | "Seamlessly integrates" | "Works with" or just remove |
|
|
54
|
+
| "Intuitive" | "Intuitive interface" | Show don't tell — describe the UX |
|
|
55
|
+
| "Comprehensive" | "Comprehensive dashboard" | Say what's in the dashboard |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Mobile-Specific Rewrites
|
|
60
|
+
|
|
61
|
+
### App Store Description
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
❌ AI:
|
|
65
|
+
TaskFlow is a powerful, comprehensive task management application that seamlessly
|
|
66
|
+
integrates with your existing workflow. By leveraging cutting-edge AI technology,
|
|
67
|
+
it significantly enhances productivity and streamlines your daily operations.
|
|
68
|
+
|
|
69
|
+
✅ Human:
|
|
70
|
+
TaskFlow keeps your team's work in one place. Add tasks, assign them, set
|
|
71
|
+
deadlines — everything syncs in real time. Works with Slack, Google Calendar,
|
|
72
|
+
and Notion.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Release Notes
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
❌ AI:
|
|
79
|
+
Version 2.1.0 introduces significant enhancements to the overall user experience,
|
|
80
|
+
including robust improvements to performance and reliability.
|
|
81
|
+
|
|
82
|
+
✅ Human:
|
|
83
|
+
2.1.0
|
|
84
|
+
- Faster load times on older Android devices (was 4s, now 1.2s)
|
|
85
|
+
- Fixed crash when opening notifications while offline
|
|
86
|
+
- Dark mode now remembers your setting between sessions
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Messages
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
❌ AI:
|
|
93
|
+
We apologize for the inconvenience. An unexpected error has occurred while
|
|
94
|
+
processing your request. Please try again later.
|
|
95
|
+
|
|
96
|
+
✅ Human:
|
|
97
|
+
Couldn't save your changes — no internet connection.
|
|
98
|
+
[Try again] [Save for later]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Onboarding Copy
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
❌ AI:
|
|
105
|
+
Welcome to our powerful platform! By leveraging our intuitive interface,
|
|
106
|
+
you'll be able to seamlessly manage all your tasks efficiently.
|
|
107
|
+
|
|
108
|
+
✅ Human:
|
|
109
|
+
Where do you want to start?
|
|
110
|
+
[Import from Trello] [Start from scratch] [Use a template]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Push Notifications
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
❌ AI:
|
|
117
|
+
You have received a new message from a team member regarding an important update.
|
|
118
|
+
|
|
119
|
+
✅ Human:
|
|
120
|
+
Alex commented on "Landing page redesign"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Empty States
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
❌ AI:
|
|
127
|
+
No items found. Get started by creating your first item to begin
|
|
128
|
+
leveraging the full power of the platform.
|
|
129
|
+
|
|
130
|
+
✅ Human:
|
|
131
|
+
No tasks yet.
|
|
132
|
+
[Add your first task]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Two-Pass Process
|
|
138
|
+
|
|
139
|
+
### Pass 1 — Rewrite
|
|
140
|
+
1. Find all patterns from the table above
|
|
141
|
+
2. Replace each with direct, specific language
|
|
142
|
+
3. Remove filler words: "overall", "various", "multiple", "utilize", "leverage"
|
|
143
|
+
4. Cut sentence length by 30%
|
|
144
|
+
|
|
145
|
+
### Pass 2 — Anti-AI Audit
|
|
146
|
+
Ask: *"What makes this obviously AI-generated?"*
|
|
147
|
+
- Does it make a claim without evidence? → Add a number or cut the claim
|
|
148
|
+
- Does it use an adjective where a verb would work? → Use the verb
|
|
149
|
+
- Could this describe any app in the category? → Make it specific to THIS app
|
|
150
|
+
- Would a person actually say this out loud? → If no, rewrite it
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Mobile Copy Principles
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
1. SPECIFIC BEATS VAGUE
|
|
158
|
+
❌ "significantly faster"
|
|
159
|
+
✅ "loads in under 1 second"
|
|
160
|
+
|
|
161
|
+
2. VERBS BEAT ADJECTIVES
|
|
162
|
+
❌ "intuitive navigation"
|
|
163
|
+
✅ "swipe left to archive"
|
|
164
|
+
|
|
165
|
+
3. SHORT SENTENCES FOR MOBILE
|
|
166
|
+
Max 12 words for notifications
|
|
167
|
+
Max 2 sentences for error messages
|
|
168
|
+
Max 3 sentences for onboarding screens
|
|
169
|
+
|
|
170
|
+
4. SHOW DON'T TELL
|
|
171
|
+
❌ "powerful search"
|
|
172
|
+
✅ "search by name, tag, due date, or assignee"
|
|
173
|
+
|
|
174
|
+
5. USER BENEFIT, NOT FEATURE
|
|
175
|
+
❌ "real-time sync enabled"
|
|
176
|
+
✅ "your team sees changes instantly"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## App Store Character Limits
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
iOS App Store:
|
|
185
|
+
Title: 30 chars → short, keyword-rich, no taglines
|
|
186
|
+
Subtitle: 30 chars → 1 benefit, not a repeat of title
|
|
187
|
+
Description: 4000 chars → first 3 lines show before "More", make them count
|
|
188
|
+
Keywords: 100 chars → comma-separated, no spaces, no repeats from title
|
|
189
|
+
What's New: 4000 chars → bullet points, plain language, no marketing
|
|
190
|
+
|
|
191
|
+
Google Play:
|
|
192
|
+
Title: 30 chars
|
|
193
|
+
Short desc: 80 chars → shows in search results
|
|
194
|
+
Full desc: 4000 chars
|
|
195
|
+
What's New: 500 chars
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
❌ Title: "TaskFlow - Ultimate Productivity & Task Management Solution"
|
|
200
|
+
✅ Title: "TaskFlow: Team Tasks & Projects"
|
|
201
|
+
|
|
202
|
+
❌ Subtitle: "The most powerful way to manage your workflow seamlessly"
|
|
203
|
+
✅ Subtitle: "Shared tasks with real-time sync"
|
|
204
|
+
|
|
205
|
+
❌ What's New: "This update introduces significant improvements to the overall
|
|
206
|
+
user experience with enhanced performance and reliability."
|
|
207
|
+
✅ What's New:
|
|
208
|
+
• Fixed crash on iPhone 14 when swiping between projects
|
|
209
|
+
• Dark mode now loads instantly instead of flashing white
|
|
210
|
+
• Added swipe-to-complete on task cards
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Permission Request Copy
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
❌ AI default (iOS):
|
|
219
|
+
"[App] Would Like to Access Your Camera"
|
|
220
|
+
(No context, user taps Don't Allow)
|
|
221
|
+
|
|
222
|
+
✅ Custom NSCameraUsageDescription:
|
|
223
|
+
"To scan receipts and attach photos to expenses"
|
|
224
|
+
|
|
225
|
+
❌ "Notifications" permission with no context
|
|
226
|
+
|
|
227
|
+
✅ Request at the right moment + explain:
|
|
228
|
+
"Get notified when teammates comment on your tasks"
|
|
229
|
+
[Allow] [Not now]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Common permission descriptions:
|
|
233
|
+
| Permission | Bad | Good |
|
|
234
|
+
|------------|-----|------|
|
|
235
|
+
| Camera | "Access camera" | "Scan QR codes to join a workspace" |
|
|
236
|
+
| Location | "Use your location" | "Show nearby team members on the map" |
|
|
237
|
+
| Contacts | "Access contacts" | "Invite teammates by name instead of email" |
|
|
238
|
+
| Notifications | "Send notifications" | "Alert you when your order ships" |
|
|
239
|
+
| Microphone | "Access microphone" | "Record voice notes on tasks" |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Rating Prompt Copy
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
❌ AI default:
|
|
247
|
+
"Are you enjoying [App Name]? Rate us 5 stars!"
|
|
248
|
+
|
|
249
|
+
✅ Human — ask a real question first:
|
|
250
|
+
"Is [App] helping you get things done?"
|
|
251
|
+
[Yes!] [Not really]
|
|
252
|
+
|
|
253
|
+
If Yes → "Mind leaving a review? It helps us a lot."
|
|
254
|
+
[Sure] [Maybe later]
|
|
255
|
+
If No → "What's getting in the way?"
|
|
256
|
+
[Give feedback]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Subscription & Paywall Copy
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
❌ AI:
|
|
265
|
+
"Unlock the full potential of our comprehensive premium features
|
|
266
|
+
to seamlessly enhance your productivity experience."
|
|
267
|
+
|
|
268
|
+
✅ Human — state what unlocks:
|
|
269
|
+
"Go Pro
|
|
270
|
+
• Unlimited projects (free plan: 3)
|
|
271
|
+
• Team sharing
|
|
272
|
+
• Priority support"
|
|
273
|
+
[Start 7-day free trial]
|
|
274
|
+
[See what's included]
|
|
275
|
+
|
|
276
|
+
❌ CTA: "Subscribe Now" / "Upgrade Today" / "Get Premium"
|
|
277
|
+
✅ CTA: "Start free trial" / "Unlock [specific feature]" / "Try Pro free"
|
|
278
|
+
|
|
279
|
+
❌ After trial ends: "Your trial has expired. Please subscribe to continue."
|
|
280
|
+
✅ "Your free trial ended. Pick a plan to keep your 12 projects."
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Word Blacklist (delete on sight)
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
seamlessly / robust / powerful / comprehensive / intuitive / streamline /
|
|
289
|
+
leverage / utilize / cutting-edge / state-of-the-art / innovative /
|
|
290
|
+
revolutionary / game-changing / transformative / holistic / synergy /
|
|
291
|
+
delve / Furthermore / Additionally / Moreover / In conclusion /
|
|
292
|
+
It is worth noting / Please note that / Feel free to /
|
|
293
|
+
experience the difference / take your [X] to the next level /
|
|
294
|
+
designed with you in mind / your one-stop solution
|
|
295
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buivietphi/skill-mobile-mt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
|
|
5
5
|
"author": "buivietphi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"flutter/",
|
|
32
32
|
"ios/",
|
|
33
33
|
"android/",
|
|
34
|
-
"shared/"
|
|
34
|
+
"shared/",
|
|
35
|
+
"humanizer/"
|
|
35
36
|
],
|
|
36
37
|
"scripts": {
|
|
37
38
|
"postinstall": "node bin/install.mjs --auto",
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Offline-First — Mobile Data Strategy
|
|
2
|
+
|
|
3
|
+
> On-demand. Load when: "offline", "offline-first", "cache", "sync", "local database", "persistence"
|
|
4
|
+
> Source: Mattermost, Immich, Expensify, Ignite
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌─────────────────────────────────────┐
|
|
12
|
+
│ UI Layer (reads local only) │
|
|
13
|
+
└──────────────┬──────────────────────┘
|
|
14
|
+
│
|
|
15
|
+
┌──────────────▼──────────────────────┐
|
|
16
|
+
│ Repository Layer (sync logic) │
|
|
17
|
+
└──────┬──────────────────────┬───────┘
|
|
18
|
+
│ │
|
|
19
|
+
┌──────▼──────┐ ┌─────────▼───────┐
|
|
20
|
+
│ Local DB │◄───►│ Remote API │
|
|
21
|
+
│ (primary) │ │ (sync only) │
|
|
22
|
+
└─────────────┘ └─────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Rule:** UI always reads local. API is sync-only, never primary.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## React Native — WatermelonDB (Reference Implementation)
|
|
30
|
+
|
|
31
|
+
### Schema
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// db/schema.ts
|
|
35
|
+
export const schema = appSchema({
|
|
36
|
+
version: 1,
|
|
37
|
+
tables: [
|
|
38
|
+
tableSchema({
|
|
39
|
+
name: 'posts',
|
|
40
|
+
columns: [
|
|
41
|
+
{ name: 'title', type: 'string' },
|
|
42
|
+
{ name: 'body', type: 'string' },
|
|
43
|
+
{ name: 'author_id', type: 'string' },
|
|
44
|
+
{ name: 'created_at', type: 'number' },
|
|
45
|
+
{ name: 'updated_at', type: 'number' },
|
|
46
|
+
{ name: 'is_synced', type: 'boolean' },
|
|
47
|
+
{ name: 'is_deleted', type: 'boolean' }, // soft delete required for sync
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Model
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export default class Post extends Model {
|
|
58
|
+
static table = 'posts';
|
|
59
|
+
static associations = {
|
|
60
|
+
comments: { type: 'has_many', foreignKey: 'post_id' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
@field('title') title!: string;
|
|
64
|
+
@field('body') body!: string;
|
|
65
|
+
@field('author_id') authorId!: string;
|
|
66
|
+
@readonly @date('created_at') createdAt!: Date;
|
|
67
|
+
@field('updated_at') updatedAt!: number;
|
|
68
|
+
@field('is_synced') isSynced!: boolean;
|
|
69
|
+
@field('is_deleted') isDeleted!: boolean;
|
|
70
|
+
|
|
71
|
+
async markAsDeleted() {
|
|
72
|
+
await this.update(post => {
|
|
73
|
+
post.isDeleted = true;
|
|
74
|
+
post.isSynced = false;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Sync Engine
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
export async function syncDatabase() {
|
|
84
|
+
await synchronize({
|
|
85
|
+
database,
|
|
86
|
+
pullChanges: async ({ lastPulledAt }) => {
|
|
87
|
+
const { changes, timestamp } = await api.get('/sync', {
|
|
88
|
+
params: { last_pulled_at: lastPulledAt },
|
|
89
|
+
});
|
|
90
|
+
return { changes, timestamp };
|
|
91
|
+
},
|
|
92
|
+
pushChanges: async ({ changes, lastPulledAt }) => {
|
|
93
|
+
await api.post('/sync', { changes, last_pulled_at: lastPulledAt });
|
|
94
|
+
},
|
|
95
|
+
migrationsEnabledAtVersion: 1,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function startBackgroundSync() {
|
|
100
|
+
const interval = setInterval(syncDatabase, 30000);
|
|
101
|
+
AppState.addEventListener('change', state => {
|
|
102
|
+
if (state === 'active') syncDatabase();
|
|
103
|
+
});
|
|
104
|
+
return () => clearInterval(interval);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Repository
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
export class PostRepository {
|
|
112
|
+
private posts = database.get<Post>('posts');
|
|
113
|
+
|
|
114
|
+
getAll() {
|
|
115
|
+
return this.posts.query(
|
|
116
|
+
Q.where('is_deleted', false),
|
|
117
|
+
Q.sortBy('created_at', Q.desc),
|
|
118
|
+
).fetch();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
observeAll() {
|
|
122
|
+
return this.posts.query(
|
|
123
|
+
Q.where('is_deleted', false),
|
|
124
|
+
Q.sortBy('created_at', Q.desc),
|
|
125
|
+
).observe();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async create(data: { title: string; body: string; authorId: string }) {
|
|
129
|
+
return database.write(async () => {
|
|
130
|
+
return this.posts.create(post => {
|
|
131
|
+
Object.assign(post, data);
|
|
132
|
+
post.isSynced = false;
|
|
133
|
+
post.isDeleted = false;
|
|
134
|
+
post.updatedAt = Date.now();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async update(post: Post, data: Partial<{ title: string; body: string }>) {
|
|
140
|
+
return database.write(async () =>
|
|
141
|
+
post.update(p => { Object.assign(p, data); p.isSynced = false; p.updatedAt = Date.now(); })
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async delete(post: Post) {
|
|
146
|
+
return database.write(async () => post.markAsDeleted());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Flutter — Drift + Riverpod
|
|
154
|
+
|
|
155
|
+
### Key differences from RN
|
|
156
|
+
|
|
157
|
+
```dart
|
|
158
|
+
// Table definition
|
|
159
|
+
class Posts extends Table {
|
|
160
|
+
TextColumn get id => text()();
|
|
161
|
+
TextColumn get title => text()();
|
|
162
|
+
BoolColumn get isSynced => boolean().withDefault(const Constant(false))();
|
|
163
|
+
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
|
|
164
|
+
@override Set<Column> get primaryKey => {id};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Watch (reactive, like observeAll)
|
|
168
|
+
Stream<List<Post>> watchAllPosts() => (select(posts)
|
|
169
|
+
..where((p) => p.isDeleted.equals(false))
|
|
170
|
+
..orderBy([(p) => Ordering.desc(p.createdAt)]))
|
|
171
|
+
.watch();
|
|
172
|
+
|
|
173
|
+
// Soft delete
|
|
174
|
+
Future<void> softDeletePost(String id) => (update(posts)).write(
|
|
175
|
+
PostsCompanion(id: Value(id), isDeleted: const Value(true), isSynced: const Value(false)),
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```dart
|
|
180
|
+
// Sync: check connectivity first
|
|
181
|
+
Future<void> sync() async {
|
|
182
|
+
final result = await Connectivity().checkConnectivity();
|
|
183
|
+
if (result == ConnectivityResult.none) return;
|
|
184
|
+
await _pushChanges();
|
|
185
|
+
await _pullChanges();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Background sync on connectivity restore
|
|
189
|
+
Connectivity().onConnectivityChanged.listen((result) {
|
|
190
|
+
if (result != ConnectivityResult.none) sync();
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## iOS — SwiftData (iOS 17+) / Core Data
|
|
197
|
+
|
|
198
|
+
```swift
|
|
199
|
+
@Model final class Post {
|
|
200
|
+
@Attribute(.unique) var id: String
|
|
201
|
+
var title: String
|
|
202
|
+
var isSynced: Bool = false
|
|
203
|
+
var isDeleted: Bool = false
|
|
204
|
+
var updatedAt: Date?
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sync with NWPathMonitor
|
|
208
|
+
private func startNetworkMonitoring() {
|
|
209
|
+
monitor.pathUpdateHandler = { [weak self] path in
|
|
210
|
+
Task { @MainActor in
|
|
211
|
+
if path.status == .satisfied { await self?.sync() }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
monitor.start(queue: queue)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func sync() async {
|
|
218
|
+
guard isConnected else { return }
|
|
219
|
+
let unsynced = try await repository.fetchUnsynced()
|
|
220
|
+
for post in unsynced {
|
|
221
|
+
post.isDeleted
|
|
222
|
+
? try await apiClient.deletePost(post.id)
|
|
223
|
+
: try await apiClient.upsertPost(post)
|
|
224
|
+
post.isSynced = true
|
|
225
|
+
}
|
|
226
|
+
let remote = try await apiClient.fetchPosts()
|
|
227
|
+
for post in remote { try await repository.upsert(post) }
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Android — Room + WorkManager
|
|
234
|
+
|
|
235
|
+
```kotlin
|
|
236
|
+
@Entity(tableName = "posts")
|
|
237
|
+
data class PostEntity(
|
|
238
|
+
@PrimaryKey val id: String,
|
|
239
|
+
val title: String,
|
|
240
|
+
val isSynced: Boolean = false,
|
|
241
|
+
val isDeleted: Boolean = false,
|
|
242
|
+
val updatedAt: Long? = null,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
@Dao interface PostDao {
|
|
246
|
+
@Query("SELECT * FROM posts WHERE isDeleted = 0 ORDER BY createdAt DESC")
|
|
247
|
+
fun observeAll(): Flow<List<PostEntity>>
|
|
248
|
+
|
|
249
|
+
@Query("SELECT * FROM posts WHERE isSynced = 0")
|
|
250
|
+
suspend fun getUnsynced(): List<PostEntity>
|
|
251
|
+
|
|
252
|
+
@Query("UPDATE posts SET isDeleted = 1, isSynced = 0 WHERE id = :id")
|
|
253
|
+
suspend fun softDelete(id: String)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Use WorkManager for background sync (respects battery/network constraints)
|
|
257
|
+
fun triggerSync() {
|
|
258
|
+
val request = OneTimeWorkRequestBuilder<SyncWorker>()
|
|
259
|
+
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
|
260
|
+
.build()
|
|
261
|
+
WorkManager.getInstance(context).enqueue(request)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Conflict Resolution
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// 1. Last Write Wins (simple, most apps)
|
|
271
|
+
const resolve = (local: Post, remote: Post) =>
|
|
272
|
+
local.updatedAt > remote.updatedAt ? local : remote;
|
|
273
|
+
|
|
274
|
+
// 2. Field-Level Merge (collaborative editing)
|
|
275
|
+
const merge = (local: Post, remote: Post, base: Post) => ({
|
|
276
|
+
...base,
|
|
277
|
+
title: local.title !== base.title ? local.title : remote.title,
|
|
278
|
+
body: local.body !== base.body ? local.body : remote.body,
|
|
279
|
+
updatedAt: Date.now(),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// 3. Operational Transform (counters/accumulators)
|
|
283
|
+
const resolveCounter = (local: Counter, remote: Counter, base: Counter) => ({
|
|
284
|
+
...remote,
|
|
285
|
+
value: base.value + (local.value - base.value) + (remote.value - base.value),
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## UI Components
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Offline banner
|
|
295
|
+
export function OfflineBanner() {
|
|
296
|
+
const [isOffline, setIsOffline] = useState(false);
|
|
297
|
+
useEffect(() => NetInfo.addEventListener(s => setIsOffline(!s.isConnected)), []);
|
|
298
|
+
if (!isOffline) return null;
|
|
299
|
+
return <Banner message="Offline — changes sync when connected." />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Sync status
|
|
303
|
+
type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error';
|
|
304
|
+
const STATUS_CONFIG = {
|
|
305
|
+
synced: { icon: 'check-circle', color: 'green' },
|
|
306
|
+
pending: { icon: 'clock', color: 'orange' },
|
|
307
|
+
syncing: { icon: 'sync', color: 'blue' },
|
|
308
|
+
error: { icon: 'alert-circle', color: 'red' },
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Optimistic update with rollback
|
|
312
|
+
const updateOptimistic = async (id: string, updates: Partial<Post>) => {
|
|
313
|
+
const prev = queryClient.getQueryData(['posts', id]);
|
|
314
|
+
queryClient.setQueryData(['posts', id], old => ({ ...old, ...updates }));
|
|
315
|
+
try {
|
|
316
|
+
await repository.update(id, updates);
|
|
317
|
+
await syncDatabase();
|
|
318
|
+
} catch {
|
|
319
|
+
queryClient.setQueryData(['posts', id], prev);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Database Selection
|
|
327
|
+
|
|
328
|
+
| DB | Platform | Use when |
|
|
329
|
+
|----|----------|----------|
|
|
330
|
+
| **WatermelonDB** | React Native | Complex queries, observables, built-in sync |
|
|
331
|
+
| **MMKV** | React Native | Key-value only, speed critical |
|
|
332
|
+
| **Realm** | RN / Flutter / iOS | Cross-platform, reactive |
|
|
333
|
+
| **SQLite** | All | Full SQL control |
|
|
334
|
+
| **Drift** | Flutter | Type-safe, migrations, code gen |
|
|
335
|
+
| **SwiftData** | iOS 17+ | Simple models, native |
|
|
336
|
+
| **Core Data** | iOS | Complex relationships, migrations |
|
|
337
|
+
| **Room** | Android | Flow/LiveData integration |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Checklist
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
Data:
|
|
345
|
+
□ All data written to local DB first
|
|
346
|
+
□ Soft deletes (never hard delete)
|
|
347
|
+
□ isSynced flag per record
|
|
348
|
+
□ Conflict resolution strategy defined
|
|
349
|
+
□ Retry for failed syncs
|
|
350
|
+
|
|
351
|
+
UI:
|
|
352
|
+
□ Offline banner
|
|
353
|
+
□ Sync status indicator
|
|
354
|
+
□ Optimistic updates
|
|
355
|
+
□ Pull-to-refresh triggers sync
|
|
356
|
+
|
|
357
|
+
Sync:
|
|
358
|
+
□ Sync on app foreground
|
|
359
|
+
□ Sync on connectivity restore
|
|
360
|
+
□ Background sync (30s interval)
|
|
361
|
+
□ Sync doesn't block UI thread
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Anti-Patterns
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
❌ UI reads directly from API
|
|
370
|
+
❌ Blocking UI during sync
|
|
371
|
+
❌ Hard deletes (breaks sync)
|
|
372
|
+
❌ Syncing on every keystroke
|
|
373
|
+
❌ No retry for failed syncs
|
|
374
|
+
❌ No offline indicator
|
|
375
|
+
❌ Assuming network is available
|
|
376
|
+
❌ Losing data on conflict
|
|
377
|
+
```
|
|
@@ -144,6 +144,86 @@ export const LoginScreen = Platform.select({
|
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
+
## iOS Haptics
|
|
148
|
+
|
|
149
|
+
```swift
|
|
150
|
+
// 3 feedback types — use the right one
|
|
151
|
+
UIImpactFeedbackGenerator(style: .medium).impactOccurred() // button tap, card flip
|
|
152
|
+
UINotificationFeedbackGenerator().notificationOccurred(.success) // save success / error / warning
|
|
153
|
+
UISelectionFeedbackGenerator().selectionChanged() // picker scroll, toggle
|
|
154
|
+
|
|
155
|
+
// ✅ Rules
|
|
156
|
+
// - Impact: physical interactions (drag drop, button press)
|
|
157
|
+
// - Notification: outcomes (success, error, warning) — max 1 per action
|
|
158
|
+
// - Selection: discrete value changes (picker, slider step)
|
|
159
|
+
// ⛔ Never chain multiple haptics in <300ms
|
|
160
|
+
// ⛔ Never use for routine navigation (back, tab switch)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Permission Timing (iOS/Android)
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
RULE: Ask ONLY when the feature needs it — not at launch
|
|
167
|
+
|
|
168
|
+
Permission When to ask
|
|
169
|
+
─────────────────────────────────────────────────────
|
|
170
|
+
Camera User taps "Take Photo" button
|
|
171
|
+
Location User taps "Find Nearby" or map feature
|
|
172
|
+
Contacts User taps "Invite from Contacts"
|
|
173
|
+
Notifications After onboarding, show a pre-permission dialog first
|
|
174
|
+
Microphone User taps "Record Voice Note"
|
|
175
|
+
|
|
176
|
+
PRE-PERMISSION DIALOG (iOS — before system prompt):
|
|
177
|
+
"Get notified when teammates reply"
|
|
178
|
+
[Allow] [Not now]
|
|
179
|
+
→ Only show system prompt if user taps Allow
|
|
180
|
+
→ Saves 1 chance at permission — don't waste it at cold start
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Ratings Timing
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
// 2-step flow — ask only after success
|
|
187
|
+
Step 1: "Is [App] helping you get things done?"
|
|
188
|
+
[Yes!] [Not really]
|
|
189
|
+
|
|
190
|
+
Step 2 (if Yes): "Mind leaving a review? It helps us a lot."
|
|
191
|
+
[Sure] [Maybe later]
|
|
192
|
+
Step 2 (if No): "What's getting in the way?" [Give feedback]
|
|
193
|
+
|
|
194
|
+
// iOS: Use SKStoreReviewController.requestReview() — max 3x/year
|
|
195
|
+
// Android: Use ReviewManager from Play Core library
|
|
196
|
+
// NEVER ask after an error, payment, or on app cold start
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Live Activities / Dynamic Island (iOS 16.1+)
|
|
200
|
+
|
|
201
|
+
```swift
|
|
202
|
+
// 1. Define attributes
|
|
203
|
+
struct DeliveryAttributes: ActivityAttributes {
|
|
204
|
+
struct ContentState: Codable, Hashable {
|
|
205
|
+
var status: String
|
|
206
|
+
var eta: Date
|
|
207
|
+
}
|
|
208
|
+
var orderId: String
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. Start activity
|
|
212
|
+
let initialState = DeliveryAttributes.ContentState(status: "Preparing", eta: Date())
|
|
213
|
+
let activity = try? Activity.request(
|
|
214
|
+
attributes: DeliveryAttributes(orderId: "123"),
|
|
215
|
+
content: .init(state: initialState, staleDate: nil)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// 3. Update
|
|
219
|
+
await activity?.update(.init(state: .init(status: "Out for delivery", eta: Date()), staleDate: nil))
|
|
220
|
+
|
|
221
|
+
// 4. End
|
|
222
|
+
await activity?.end(dismissalPolicy: .default)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
147
227
|
## Anti-Patterns
|
|
148
228
|
|
|
149
229
|
```
|
|
@@ -151,9 +231,14 @@ export const LoginScreen = Platform.select({
|
|
|
151
231
|
❌ iOS navigation on Android
|
|
152
232
|
❌ Ignoring platform conventions
|
|
153
233
|
❌ "Write once, look mediocre everywhere"
|
|
234
|
+
❌ Asking permissions at app launch
|
|
235
|
+
❌ Chaining multiple haptics back-to-back
|
|
236
|
+
❌ Rating prompt right after install
|
|
154
237
|
|
|
155
238
|
✅ Native look & feel per platform
|
|
156
239
|
✅ Shared logic, platform UI
|
|
157
240
|
✅ Respect platform guidelines
|
|
158
241
|
✅ "Write once, look native everywhere"
|
|
242
|
+
✅ Ask permissions at the moment they're needed
|
|
243
|
+
✅ Rating prompts only after clear success moments
|
|
159
244
|
```
|