diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index ebf2aa9..f8b7c6d 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -1,8 +1,8 @@
{
"name": "mobile-app-developer-tools",
"displayName": "Mobile App Developer Tools",
- "version": "0.6.0",
- "description": "Mobile app development for Cursor, Claude Code, and MCP-compatible editors. 20 skills covering React Native/Expo and Flutter - project setup through app store submission - plus 6 rules. Companion MCP server provides 15 tools.",
+ "version": "0.7.0",
+ "description": "Mobile app development for Cursor, Claude Code, and MCP-compatible editors. 24 skills covering React Native/Expo and Flutter - project setup through app store submission, monetization, analytics, and OTA updates - plus 7 rules. Companion MCP server provides 19 tools.",
"author": {
"name": "TMHSDigital",
"url": "https://github.com/TMHSDigital"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8162e8..459a610 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
+## [0.7.0] - 2026-04-03
+
+### Added
+
+- **4 new skills**: `mobile-monetization` (in-app purchases, subscriptions, RevenueCat, StoreKit 2), `mobile-deep-links` (universal links, app links, URL schemes, deferred deep links), `mobile-analytics` (Sentry, Firebase Crashlytics, PostHog, source maps, GDPR), `mobile-ota-updates` (EAS Update, channels, staged rollouts, rollback, Shorebird)
+- **1 new rule**: `mobile-bundle-size` (flags large dependencies, unoptimized imports, heavy packages with lighter alternatives)
+- **4 new MCP tools**: `mobile_submitToPlayStore`, `mobile_generateScreenshots`, `mobile_analyzeBundle`, `mobile_configureOTA`
+- Totals: 24 skills, 7 rules, 19 MCP tools
+
## [0.6.0] - 2026-04-03
### Added
diff --git a/CLAUDE.md b/CLAUDE.md
index 7529e8e..837b3e7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-The **Mobile App Developer Tools** Cursor plugin is at **v0.6.0**. It helps developers go from zero to a published app in the stores. Supports React Native/Expo and Flutter with **20 skills**, **6 rules**, and a companion MCP server exposing **15 tools**.
+The **Mobile App Developer Tools** Cursor plugin is at **v0.7.0**. It helps developers go from zero to a published app in the stores. Supports React Native/Expo and Flutter with **24 skills**, **7 rules**, and a companion MCP server exposing **19 tools**.
## Demo App
@@ -16,11 +16,11 @@ The **Mobile App Developer Tools** Cursor plugin is at **v0.6.0**. It helps deve
.cursor-plugin/plugin.json - Plugin manifest
skills//SKILL.md - AI workflow definitions
rules/.mdc - Code quality and security rules
-mcp-server/ - MCP server with 15 tools
+mcp-server/ - MCP server with 19 tools
packages/mobile-dev-tools/ - NPM package (stub for name claim)
```
-## Skills (20 total)
+## Skills (24 total)
### React Native / Expo
@@ -56,8 +56,12 @@ packages/mobile-dev-tools/ - NPM package (stub for name claim)
| Skill | Purpose |
| --- | --- |
| mobile-app-store-prep | App icons, screenshots, metadata, privacy policy, age ratings, review guidelines |
+| mobile-monetization | In-app purchases, subscriptions, RevenueCat, StoreKit 2, sandbox testing |
+| mobile-deep-links | Universal links, app links, URL schemes, deferred deep links, attribution |
+| mobile-analytics | Crash reporting (Sentry, Crashlytics), event tracking (PostHog), source maps |
+| mobile-ota-updates | EAS Update channels, runtime versions, staged rollouts, rollback, Shorebird |
-## Rules (6 total)
+## Rules (7 total)
| Rule | Scope | Purpose |
| --- | --- | --- |
@@ -67,12 +71,13 @@ packages/mobile-dev-tools/ - NPM package (stub for name claim)
| mobile-env-safety.mdc | `.ts`, `.tsx`, `.json` | Flags hardcoded production endpoints, missing EXPO_PUBLIC_ prefix, server-only secrets in client code |
| mobile-performance.mdc | `.ts`, `.tsx`, `.dart` | Flags inline styles, missing list keys, unnecessary re-renders (RN); missing const constructors, inline widgets (Flutter) |
| mobile-accessibility.mdc | `.ts`, `.tsx`, `.dart` | Flags missing a11y labels, small touch targets, images without alt text, color-only indicators |
+| mobile-bundle-size.mdc | `.ts`, `.tsx`, `.json`, `.dart` | Flags large dependencies, unoptimized imports, heavy packages with lighter alternatives |
## Companion MCP Server
Tools use the `mobile_` prefix (for example `mobile_checkDevEnvironment`).
-### Tools (15 total)
+### Tools (19 total)
| Tool | Description |
| --- | --- |
@@ -91,6 +96,10 @@ Tools use the `mobile_` prefix (for example `mobile_checkDevEnvironment`).
| mobile_buildForStore | Create a production build for app store submission via EAS Build |
| mobile_validateStoreMetadata | Check app.json for all required store listing fields (name, bundle ID, icon, etc.) |
| mobile_submitToAppStore | Submit latest iOS production build to App Store Connect via EAS Submit |
+| mobile_submitToPlayStore | Submit latest Android production build to Google Play Console via EAS Submit |
+| mobile_generateScreenshots | Generate screenshot capture script and list required store dimensions |
+| mobile_analyzeBundle | Analyze app bundle for large dependencies, heavy assets, and optimization opportunities |
+| mobile_configureOTA | Configure EAS Update for over-the-air JavaScript updates with channels and runtime versions |
## Development Workflow
diff --git a/README.md b/README.md
index e4f9b40..8635c68 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -24,7 +24,7 @@
---
- 20 skills • 6 rules • 15 MCP tools
+ 24 skills • 7 rules • 19 MCP tools
@@ -35,15 +35,15 @@
## Overview
-Mobile App Developer Tools is a **Cursor** plugin by **TMHSDigital** that packages agent skills, editor rules, and a TypeScript **MCP server** (`mcp-server/`) so you can scaffold, build, and ship mobile apps without leaving the IDE. Currently at **v0.6.0** with twenty skills (React Native/Expo + Flutter), six rules, and fifteen live MCP tools.
+Mobile App Developer Tools is a **Cursor** plugin by **TMHSDigital** that packages agent skills, editor rules, and a TypeScript **MCP server** (`mcp-server/`) so you can scaffold, build, and ship mobile apps without leaving the IDE. Currently at **v0.7.0** with twenty-four skills (React Native/Expo + Flutter), seven rules, and nineteen live MCP tools.
**What you get**
| Layer | Role |
| --- | --- |
-| **Skills** | 20 guided workflows for React Native/Expo and Flutter: project setup through app store submission |
-| **Rules** | 6 guardrails: secrets, platform guards, image bloat, env safety, performance, accessibility |
-| **MCP** | 15 tools: env checks, scaffolding, device deploy, screen/component gen, permissions, AI, build health, push, deep links, store builds, metadata validation, App Store submission |
+| **Skills** | 24 guided workflows for React Native/Expo and Flutter: project setup through monetization, analytics, and OTA updates |
+| **Rules** | 7 guardrails: secrets, platform guards, image bloat, env safety, performance, accessibility, bundle size |
+| **MCP** | 19 tools: env checks, scaffolding, device deploy, screen/component gen, permissions, AI, build health, push, deep links, store builds, metadata validation, App Store + Play Store submission, screenshots, bundle analysis, OTA config |
**Quick facts**
@@ -61,7 +61,7 @@ Mobile App Developer Tools is a **Cursor** plugin by **TMHSDigital** that packag
flowchart LR
A[User asks mobile dev question] --> B[Cursor loads a Skill]
B --> C{MCP server configured?}
- C -->|Yes| D["mobile-mcp tools (15)"]
+ C -->|Yes| D["mobile-mcp tools (19)"]
C -->|No| E[Docs-only guidance]
D --> F[Local env checks / scaffolding]
E --> G[Answer in chat or code edits]
@@ -144,7 +144,7 @@ Open Cursor and ask:
## Demo App
-See the plugin in action: **[SnapLog](https://github.com/TMHSDigital/Demo-Mobile-App)** is a photo journal app built entirely using these skills and MCP tools. It exercises 16 of the 20 skills - from project scaffolding and navigation to camera capture, AI descriptions, local storage, and push notifications.
+See the plugin in action: **[SnapLog](https://github.com/TMHSDigital/Demo-Mobile-App)** is a photo journal app built entirely using these skills and MCP tools. It exercises 16 of the 24 skills - from project scaffolding and navigation to camera capture, AI descriptions, local storage, and push notifications.
[](https://github.com/TMHSDigital/Demo-Mobile-App)
@@ -152,7 +152,7 @@ See the plugin in action: **[SnapLog](https://github.com/TMHSDigital/Demo-Mobile
## Skills
-All 20 skills are production-ready. Names match the folder under `skills/`.
+All 24 skills are production-ready. Names match the folder under `skills/`.
React Native / Expo skills (15)
@@ -190,11 +190,15 @@ All 20 skills are production-ready. Names match the folder under `skills/`.
-Shared skills (1)
+Shared skills (5)
| Skill | What it does |
| --- | --- |
| `mobile-app-store-prep` | App icons, screenshots, metadata, privacy policy, age ratings, review guidelines |
+| `mobile-monetization` | In-app purchases, subscriptions, RevenueCat, StoreKit 2, sandbox testing, price localization |
+| `mobile-deep-links` | Universal links (iOS), app links (Android), URL schemes, deferred deep links, install attribution |
+| `mobile-analytics` | Crash reporting (Sentry, Firebase Crashlytics), event tracking (PostHog), source maps, GDPR compliance |
+| `mobile-ota-updates` | EAS Update channels, runtime versions, staged rollouts, rollback, Shorebird for Flutter |
@@ -223,6 +227,10 @@ All 20 skills are production-ready. Names match the folder under `skills/`.
| `mobile-flutter-navigation` | "Add tab navigation with GoRouter in my Flutter app" |
| `mobile-flutter-run-on-device` | "My Android phone doesn't show up in flutter devices" |
| `mobile-flutter-state-management` | "Should I use Riverpod or Bloc for my Flutter app?" |
+| `mobile-monetization` | "Add a monthly subscription with a free trial using RevenueCat" |
+| `mobile-deep-links` | "Make shared links like example.com/recipe/42 open in my app" |
+| `mobile-analytics` | "Set up crash reporting with Sentry and event tracking with PostHog" |
+| `mobile-ota-updates` | "Push a bug fix to production without going through app review" |
@@ -230,10 +238,10 @@ All 20 skills are production-ready. Names match the folder under `skills/`.
## Rules
-All 6 rules are production-ready.
+All 7 rules are production-ready.
-All 6 rules
+All 7 rules
| Rule | Scope | What it catches |
| --- | --- | --- |
@@ -243,6 +251,7 @@ All 6 rules are production-ready.
| `mobile-env-safety` | `.ts`, `.tsx`, `.json` | Hardcoded production endpoints, missing `EXPO_PUBLIC_` prefix, server-only secrets in client code |
| `mobile-performance` | `.ts`, `.tsx`, `.dart` | Inline styles, missing list keys, ScrollView for long lists (RN); missing const constructors, inline widgets (Flutter) |
| `mobile-accessibility` | `.ts`, `.tsx`, `.dart` | Missing a11y labels on interactive elements, small touch targets, images without alt text, color-only indicators |
+| `mobile-bundle-size` | `.ts`, `.tsx`, `.json`, `.dart` | Large dependencies (moment, lodash, aws-sdk), unoptimized imports, heavy packages with lighter alternatives |
@@ -277,7 +286,7 @@ npx @tmhs/mobile-mcp
```
-All 15 MCP tools
+All 19 MCP tools
| Tool | Purpose |
| --- | --- |
@@ -296,6 +305,10 @@ npx @tmhs/mobile-mcp
| `mobile_buildForStore` | Create a production build for app store submission via EAS Build. Validates app.json before building. |
| `mobile_validateStoreMetadata` | Check app.json for all required store listing fields (name, bundle ID, version, icon, splash, privacy policy). |
| `mobile_submitToAppStore` | Submit the latest iOS production build to App Store Connect via EAS Submit. |
+| `mobile_submitToPlayStore` | Submit the latest Android production build to Google Play Console via EAS Submit. |
+| `mobile_generateScreenshots` | Generate a screenshot capture helper script and list required store dimensions for iOS and Android. |
+| `mobile_analyzeBundle` | Analyze app bundle for large dependencies, heavy assets, and optimization opportunities. |
+| `mobile_configureOTA` | Configure EAS Update for over-the-air JavaScript updates with channels and runtime version policy. |
@@ -357,8 +370,8 @@ Full details in [ROADMAP.md](ROADMAP.md).
| **v0.3.0** | Camera & AI | 9 skills, 3 rules, 9 MCP tools | |
| **v0.4.0** | Users & Data | 13 skills, 4 rules, 12 MCP tools | |
| **v0.5.0** | Flutter | 17 skills, 5 rules, 12 MCP tools | |
-| **v0.6.0** | Ship It | 20 skills, 6 rules, 15 MCP tools | **Current** |
-| **v0.7.0** | Grow & Measure | 24 skills, 7 rules, 19 MCP tools | |
+| **v0.6.0** | Ship It | 20 skills, 6 rules, 15 MCP tools | |
+| **v0.7.0** | Grow & Measure | 24 skills, 7 rules, 19 MCP tools | **Current** |
| **v0.8.0** | Test & Automate | 27 skills, 8 rules, 22 MCP tools | |
| **v0.9.0** | Rich Features | 32 skills, 9 rules, 26 MCP tools | |
| **v0.10.0** | Harden | 37 skills, 10 rules, 30 MCP tools | |
diff --git a/ROADMAP.md b/ROADMAP.md
index 4f33d53..088c3f0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -9,8 +9,8 @@
| **v0.3.0** | Camera & AI | +3 | +1 | +3 | Camera integration, AI features, permissions skill, image-assets rule |
| **v0.4.0** | Users & Data | +4 | +1 | +3 | Auth, push notifications, local storage, API integration, env-safety rule |
| **v0.5.0** | Flutter | +4 | +1 | +0 | Flutter project setup, navigation, run-on-device, state management, performance rule |
-| **v0.6.0** | Ship It | +3 | +1 | +3 | App store prep, iOS submission, Android submission, accessibility rule **(current)** |
-| **v0.7.0** | Grow & Measure | +4 | +1 | +4 | Monetization, deep links, analytics/crash reporting, OTA updates, bundle analysis |
+| **v0.6.0** | Ship It | +3 | +1 | +3 | App store prep, iOS submission, Android submission, accessibility rule |
+| **v0.7.0** | Grow & Measure | +4 | +1 | +4 | Monetization, deep links, analytics/crash reporting, OTA updates, bundle analysis **(current)** |
| **v0.8.0** | Test & Automate | +3 | +1 | +3 | Unit/E2E testing, CI/CD pipelines, test file generation |
| **v0.9.0** | Rich Features | +5 | +1 | +4 | Animations, maps/location, i18n, forms/validation, real-time/WebSockets |
| **v0.10.0** | Harden | +5 | +1 | +4 | Security, offline-sync, background tasks, debugging, production APM |
@@ -90,7 +90,7 @@
**Rules:**
- `mobile-performance` - Common performance anti-patterns (inline styles, missing keys, heavy re-renders)
-## v0.6.0 - Ship It (current)
+## v0.6.0 - Ship It
**Skills:**
- `mobile-app-store-prep` (Shared) - Screenshots, descriptions, metadata, review guidelines
@@ -105,7 +105,7 @@
- `mobile_validateStoreMetadata` - Check store listing fields
- `mobile_submitToAppStore` - Trigger iOS submission
-## v0.7.0 - Grow & Measure
+## v0.7.0 - Grow & Measure (current)
**Skills:**
- `mobile-monetization` (Shared) - In-app purchases, subscriptions, RevenueCat, StoreKit 2
diff --git a/mcp-server/package.json b/mcp-server/package.json
index b5ec702..717fbd1 100644
--- a/mcp-server/package.json
+++ b/mcp-server/package.json
@@ -1,7 +1,7 @@
{
"name": "@tmhs/mobile-mcp",
- "version": "0.6.0",
- "description": "MCP server for mobile app development - 15 tools for environment checks, project scaffolding, device deployment, screen/component generation, dependency installation, permissions, AI integration, build health, push notifications, deep links, dev environment reset, store builds, metadata validation, and App Store submission.",
+ "version": "0.7.0",
+ "description": "MCP server for mobile app development - 19 tools for environment checks, project scaffolding, device deployment, screen/component generation, dependency installation, permissions, AI integration, build health, push notifications, deep links, dev environment reset, store builds, metadata validation, App Store submission, Play Store submission, screenshot capture, bundle analysis, and OTA update configuration.",
"type": "module",
"main": "dist/index.js",
"bin": {
diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts
index 52b49a2..04bd44e 100644
--- a/mcp-server/src/index.ts
+++ b/mcp-server/src/index.ts
@@ -18,10 +18,14 @@ import { register as registerResetDevEnvironment } from "./tools/resetDevEnviron
import { register as registerBuildForStore } from "./tools/buildForStore.js";
import { register as registerValidateStoreMetadata } from "./tools/validateStoreMetadata.js";
import { register as registerSubmitToAppStore } from "./tools/submitToAppStore.js";
+import { register as registerSubmitToPlayStore } from "./tools/submitToPlayStore.js";
+import { register as registerGenerateScreenshots } from "./tools/generateScreenshots.js";
+import { register as registerAnalyzeBundle } from "./tools/analyzeBundle.js";
+import { register as registerConfigureOTA } from "./tools/configureOTA.js";
const server = new McpServer({
name: "mobile-mcp",
- version: "0.6.0",
+ version: "0.7.0",
});
registerCheckDevEnvironment(server);
@@ -39,6 +43,10 @@ registerResetDevEnvironment(server);
registerBuildForStore(server);
registerValidateStoreMetadata(server);
registerSubmitToAppStore(server);
+registerSubmitToPlayStore(server);
+registerGenerateScreenshots(server);
+registerAnalyzeBundle(server);
+registerConfigureOTA(server);
async function main(): Promise {
const transport = new StdioServerTransport();
diff --git a/mcp-server/src/tools/analyzeBundle.ts b/mcp-server/src/tools/analyzeBundle.ts
new file mode 100644
index 0000000..d601169
--- /dev/null
+++ b/mcp-server/src/tools/analyzeBundle.ts
@@ -0,0 +1,161 @@
+import { z } from "zod";
+import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
+import { join, extname } from "node:path";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { textResponse, errorResponse } from "../types.js";
+
+const KNOWN_HEAVY_PACKAGES: Record = {
+ moment: { size: "~300KB", alternative: "date-fns or dayjs" },
+ "moment-timezone": { size: "~200KB", alternative: "date-fns-tz" },
+ lodash: { size: "~70KB", alternative: "lodash-es or individual lodash/* imports" },
+ "aws-sdk": { size: "~50MB", alternative: "@aws-sdk/client-* (v3 modular)" },
+ firebase: { size: "~500KB", alternative: "@react-native-firebase/* (modular)" },
+ antd: { size: "~1MB", alternative: "Not suitable for React Native" },
+ "react-native-maps": { size: "~15MB native", alternative: "Only if maps are essential" },
+ underscore: { size: "~25KB", alternative: "Native Array/Object methods or lodash-es" },
+ axios: { size: "~15KB", alternative: "Built-in fetch API" },
+ "crypto-js": { size: "~40KB", alternative: "expo-crypto for basic hashing" },
+};
+
+const LARGE_ASSET_THRESHOLD = 500 * 1024;
+const ASSET_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".ttf", ".otf", ".json"]);
+
+function scanAssets(dir: string): Array<{ path: string; size: number }> {
+ const results: Array<{ path: string; size: number }> = [];
+ if (!existsSync(dir)) return results;
+
+ try {
+ const entries = readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
+ results.push(...scanAssets(fullPath));
+ } else if (entry.isFile() && ASSET_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
+ const stat = statSync(fullPath);
+ if (stat.size > LARGE_ASSET_THRESHOLD) {
+ results.push({ path: fullPath, size: stat.size });
+ }
+ }
+ }
+ } catch {
+ // Skip directories we can't read
+ }
+
+ return results;
+}
+
+function formatBytes(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+const inputSchema = {
+ project_path: z
+ .string()
+ .optional()
+ .describe("Absolute path to the project root. Defaults to cwd."),
+ platform: z
+ .enum(["ios", "android", "both"])
+ .optional()
+ .default("both")
+ .describe("Target platform for analysis context (default: both)"),
+};
+
+export function register(server: McpServer): void {
+ server.tool(
+ "mobile_analyzeBundle",
+ "Analyze app bundle for large dependencies, heavy assets, and optimization opportunities. Reads package.json and scans the project for bloat.",
+ inputSchema,
+ async (args) => {
+ try {
+ const root = args.project_path || process.cwd();
+ const pkgPath = join(root, "package.json");
+
+ if (!existsSync(pkgPath)) {
+ return errorResponse(
+ new Error(`No package.json at ${root}. Is this a Node.js project?`),
+ );
+ }
+
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
+ const deps = Object.keys(pkg.dependencies || {});
+ const devDeps = Object.keys(pkg.devDependencies || {});
+
+ const heavyDeps: Array<{
+ name: string;
+ estimated_size: string;
+ alternative: string;
+ }> = [];
+
+ for (const dep of deps) {
+ const info = KNOWN_HEAVY_PACKAGES[dep];
+ if (info) {
+ heavyDeps.push({
+ name: dep,
+ estimated_size: info.size,
+ alternative: info.alternative,
+ });
+ }
+ }
+
+ const assetsDir = join(root, "assets");
+ const largeAssets = scanAssets(assetsDir).map((a) => ({
+ path: a.path.replace(root, "."),
+ size: formatBytes(a.size),
+ size_bytes: a.size,
+ }));
+
+ const recommendations: string[] = [];
+
+ if (heavyDeps.length > 0) {
+ recommendations.push(
+ `Replace ${heavyDeps.length} heavy dependenc${heavyDeps.length === 1 ? "y" : "ies"} with lighter alternatives`,
+ );
+ }
+
+ if (largeAssets.length > 0) {
+ recommendations.push(
+ `Optimize ${largeAssets.length} large asset${largeAssets.length === 1 ? "" : "s"} (>500KB each)`,
+ );
+ recommendations.push("Convert PNG/JPEG assets to WebP for 30-50% size reduction");
+ }
+
+ if (deps.length > 50) {
+ recommendations.push(
+ `${deps.length} production dependencies is high. Run npx depcheck to find unused packages.`,
+ );
+ }
+
+ if (deps.includes("lodash") && !deps.includes("lodash-es")) {
+ recommendations.push(
+ "Switch from lodash to lodash-es for proper tree shaking",
+ );
+ }
+
+ recommendations.push("Run `npx expo export` to measure actual bundle size");
+
+ return textResponse(
+ JSON.stringify(
+ {
+ success: true,
+ summary: {
+ production_dependencies: deps.length,
+ dev_dependencies: devDeps.length,
+ heavy_packages_found: heavyDeps.length,
+ large_assets_found: largeAssets.length,
+ },
+ heavy_dependencies: heavyDeps,
+ large_assets: largeAssets,
+ recommendations,
+ },
+ null,
+ 2,
+ ),
+ );
+ } catch (err) {
+ return errorResponse(err);
+ }
+ },
+ );
+}
diff --git a/mcp-server/src/tools/configureOTA.ts b/mcp-server/src/tools/configureOTA.ts
new file mode 100644
index 0000000..52d0a1c
--- /dev/null
+++ b/mcp-server/src/tools/configureOTA.ts
@@ -0,0 +1,169 @@
+import { z } from "zod";
+import { readFileSync, writeFileSync, existsSync } from "node:fs";
+import { join } from "node:path";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { textResponse, errorResponse } from "../types.js";
+
+const inputSchema = {
+ project_path: z
+ .string()
+ .optional()
+ .describe("Absolute path to the Expo project root. Defaults to cwd."),
+ channel: z
+ .string()
+ .optional()
+ .default("production")
+ .describe(
+ "Default update channel name (default: production). Common values: production, staging, preview.",
+ ),
+ runtime_version_policy: z
+ .enum(["appVersion", "nativeVersion", "fingerprint"])
+ .optional()
+ .default("fingerprint")
+ .describe(
+ "How to determine runtime version compatibility (default: fingerprint). fingerprint auto-detects native changes.",
+ ),
+};
+
+export function register(server: McpServer): void {
+ server.tool(
+ "mobile_configureOTA",
+ "Configure EAS Update for over-the-air JavaScript updates. Sets runtime version policy and update URL in app.json, and verifies eas.json channel config.",
+ inputSchema,
+ async (args) => {
+ try {
+ const root = args.project_path || process.cwd();
+ const appJsonPath = join(root, "app.json");
+
+ if (!existsSync(appJsonPath)) {
+ return errorResponse(
+ new Error(`No app.json at ${root}. Is this an Expo project?`),
+ );
+ }
+
+ const appJson = JSON.parse(readFileSync(appJsonPath, "utf-8"));
+ const expo = appJson.expo || {};
+
+ const projectId = expo.extra?.eas?.projectId;
+ if (!projectId) {
+ return textResponse(
+ JSON.stringify(
+ {
+ success: false,
+ message:
+ "No EAS project ID found. Run `eas init` to link this project to EAS.",
+ fix: "eas init",
+ },
+ null,
+ 2,
+ ),
+ );
+ }
+
+ const changes: string[] = [];
+
+ if (args.runtime_version_policy === "fingerprint") {
+ if (
+ typeof expo.runtimeVersion !== "object" ||
+ expo.runtimeVersion?.policy !== "fingerprint"
+ ) {
+ expo.runtimeVersion = { policy: "fingerprint" };
+ changes.push(
+ 'Set runtimeVersion to { policy: "fingerprint" }',
+ );
+ }
+ } else {
+ if (
+ typeof expo.runtimeVersion !== "object" ||
+ expo.runtimeVersion?.policy !== args.runtime_version_policy
+ ) {
+ expo.runtimeVersion = { policy: args.runtime_version_policy };
+ changes.push(
+ `Set runtimeVersion to { policy: "${args.runtime_version_policy}" }`,
+ );
+ }
+ }
+
+ if (!expo.updates) {
+ expo.updates = {};
+ }
+
+ const updateUrl = `https://u.expo.dev/${projectId}`;
+ if (expo.updates.url !== updateUrl) {
+ expo.updates.url = updateUrl;
+ changes.push(`Set updates.url to ${updateUrl}`);
+ }
+
+ if (expo.updates.enabled !== true) {
+ expo.updates.enabled = true;
+ changes.push("Enabled updates");
+ }
+
+ if (expo.updates.checkAutomatically !== "ON_LOAD") {
+ expo.updates.checkAutomatically = "ON_LOAD";
+ changes.push("Set checkAutomatically to ON_LOAD");
+ }
+
+ if (expo.updates.fallbackToCacheTimeout !== 0) {
+ expo.updates.fallbackToCacheTimeout = 0;
+ changes.push(
+ "Set fallbackToCacheTimeout to 0 (loads cached bundle immediately, downloads update in background)",
+ );
+ }
+
+ appJson.expo = expo;
+
+ if (changes.length > 0) {
+ writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2) + "\n", "utf-8");
+ }
+
+ const easJsonPath = join(root, "eas.json");
+ let easJsonWarning: string | null = null;
+
+ if (!existsSync(easJsonPath)) {
+ easJsonWarning =
+ "No eas.json found. Run `eas build:configure` to create it, then add channel config to your build profiles.";
+ } else {
+ const easJson = JSON.parse(readFileSync(easJsonPath, "utf-8"));
+ const profiles = easJson.build || {};
+ const hasChannel = Object.values(profiles).some(
+ (p: any) => p.channel,
+ );
+ if (!hasChannel) {
+ easJsonWarning = `No channel configured in eas.json build profiles. Add "channel": "${args.channel}" to your production profile.`;
+ }
+ }
+
+ return textResponse(
+ JSON.stringify(
+ {
+ success: true,
+ message:
+ changes.length > 0
+ ? "EAS Update configured in app.json"
+ : "EAS Update already configured, no changes needed",
+ changes,
+ config: {
+ runtime_version_policy: args.runtime_version_policy,
+ update_url: `https://u.expo.dev/${projectId}`,
+ channel: args.channel,
+ },
+ eas_json_warning: easJsonWarning,
+ next_steps: [
+ `Ensure eas.json production profile has "channel": "${args.channel}"`,
+ "Create a production build: eas build --platform all --profile production",
+ `Publish an update: eas update --channel ${args.channel} --message "description"`,
+ "For staged rollouts: eas update --channel production --rollout-percentage 10",
+ "Monitor updates at https://expo.dev",
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+ } catch (err) {
+ return errorResponse(err);
+ }
+ },
+ );
+}
diff --git a/mcp-server/src/tools/generateScreenshots.ts b/mcp-server/src/tools/generateScreenshots.ts
new file mode 100644
index 0000000..bb7ceea
--- /dev/null
+++ b/mcp-server/src/tools/generateScreenshots.ts
@@ -0,0 +1,158 @@
+import { z } from "zod";
+import { existsSync, mkdirSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { textResponse, errorResponse } from "../types.js";
+
+const IOS_DEVICES = [
+ { name: '6.7" Display', width: 1290, height: 2796, label: "iPhone 15 Pro Max / 16 Pro Max" },
+ { name: '6.5" Display', width: 1284, height: 2778, label: "iPhone 14 Plus / 15 Plus" },
+ { name: '5.5" Display', width: 1242, height: 2208, label: "iPhone 8 Plus (legacy)" },
+ { name: '12.9" iPad Pro', width: 2048, height: 2732, label: "iPad Pro 12.9-inch" },
+];
+
+const ANDROID_DEVICES = [
+ { name: "Phone", width: 1080, height: 1920, label: "Standard phone (1080x1920)" },
+ { name: "Phone Hi-Res", width: 1440, height: 3120, label: "Flagship phone (1440x3120)" },
+ { name: '7" Tablet', width: 1200, height: 1920, label: "7-inch tablet" },
+ { name: '10" Tablet', width: 1600, height: 2560, label: "10-inch tablet" },
+];
+
+const HELPER_SCRIPT = `#!/usr/bin/env node
+/**
+ * Screenshot helper - captures screenshots at store-required dimensions.
+ * Run from your project root with a running dev server.
+ *
+ * Usage:
+ * node scripts/capture-screenshots.js --platform ios
+ * node scripts/capture-screenshots.js --platform android
+ *
+ * Prerequisites:
+ * - Simulator/emulator running with your app loaded
+ * - For iOS: Xcode command line tools (xcrun simctl)
+ * - For Android: adb in PATH
+ */
+
+const { execSync } = require("child_process");
+const { mkdirSync, existsSync } = require("fs");
+const path = require("path");
+
+const platform = process.argv.includes("--android") ? "android" : "ios";
+const outDir = path.join("screenshots", platform);
+
+if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
+
+if (platform === "ios") {
+ const timestamp = Date.now();
+ const file = path.join(outDir, \`screenshot_\${timestamp}.png\`);
+ try {
+ execSync(\`xcrun simctl io booted screenshot "\${file}"\`, { stdio: "pipe" });
+ console.log("Saved:", file);
+ } catch (e) {
+ console.error("Failed. Is the iOS Simulator running?");
+ process.exit(1);
+ }
+} else {
+ const timestamp = Date.now();
+ const file = path.join(outDir, \`screenshot_\${timestamp}.png\`);
+ try {
+ execSync(\`adb exec-out screencap -p > "\${file}"\`, { stdio: "pipe", shell: true });
+ console.log("Saved:", file);
+ } catch (e) {
+ console.error("Failed. Is an Android device/emulator connected?");
+ process.exit(1);
+ }
+}
+`;
+
+const inputSchema = {
+ project_path: z
+ .string()
+ .optional()
+ .describe("Absolute path to the Expo project root. Defaults to cwd."),
+ platform: z
+ .enum(["ios", "android", "both"])
+ .optional()
+ .default("both")
+ .describe("Target platform for screenshot dimensions (default: both)"),
+};
+
+export function register(server: McpServer): void {
+ server.tool(
+ "mobile_generateScreenshots",
+ "Generate a screenshot capture helper script and list required App Store and Play Store screenshot dimensions. Creates scripts/capture-screenshots.js.",
+ inputSchema,
+ async (args) => {
+ try {
+ const root = args.project_path || process.cwd();
+ const scriptsDir = join(root, "scripts");
+
+ if (!existsSync(scriptsDir)) {
+ mkdirSync(scriptsDir, { recursive: true });
+ }
+
+ const scriptPath = join(scriptsDir, "capture-screenshots.js");
+ writeFileSync(scriptPath, HELPER_SCRIPT, "utf-8");
+
+ const devices: Array<{
+ platform: string;
+ name: string;
+ width: number;
+ height: number;
+ label: string;
+ }> = [];
+
+ if (args.platform === "ios" || args.platform === "both") {
+ devices.push(...IOS_DEVICES.map((d) => ({ platform: "ios", ...d })));
+ }
+
+ if (args.platform === "android" || args.platform === "both") {
+ devices.push(
+ ...ANDROID_DEVICES.map((d) => ({ platform: "android", ...d })),
+ );
+ }
+
+ return textResponse(
+ JSON.stringify(
+ {
+ success: true,
+ message:
+ "Screenshot helper script created and dimension reference generated",
+ script: "scripts/capture-screenshots.js",
+ usage: [
+ "node scripts/capture-screenshots.js --ios",
+ "node scripts/capture-screenshots.js --android",
+ ],
+ required_dimensions: devices,
+ guidelines: {
+ ios: [
+ "Minimum 3 screenshots, maximum 10 per device size",
+ "6.7-inch and 6.5-inch displays are required for current iPhones",
+ "5.5-inch is required if supporting older iPhones",
+ "No device frames, status bars, or alpha transparency",
+ "PNG or JPEG, RGB color space",
+ ],
+ android: [
+ "Minimum 2 screenshots, maximum 8",
+ "JPEG or 24-bit PNG, minimum 320px, maximum 3840px per side",
+ "16:9 aspect ratio recommended for phones",
+ "At least one phone and one 7-inch or 10-inch tablet screenshot",
+ ],
+ },
+ next_steps: [
+ "Navigate to each key screen in your app",
+ "Run the capture script for each screen",
+ "Resize captures to required dimensions if needed",
+ "Upload to App Store Connect / Play Console",
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+ } catch (err) {
+ return errorResponse(err);
+ }
+ },
+ );
+}
diff --git a/mcp-server/src/tools/submitToPlayStore.ts b/mcp-server/src/tools/submitToPlayStore.ts
new file mode 100644
index 0000000..26b5c7a
--- /dev/null
+++ b/mcp-server/src/tools/submitToPlayStore.ts
@@ -0,0 +1,132 @@
+import { z } from "zod";
+import { execSync } from "node:child_process";
+import { readFileSync, existsSync } from "node:fs";
+import { join } from "node:path";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { textResponse, errorResponse } from "../types.js";
+
+const inputSchema = {
+ project_path: z
+ .string()
+ .optional()
+ .describe("Absolute path to the Expo project root. Defaults to cwd."),
+ track: z
+ .enum(["internal", "alpha", "beta", "production"])
+ .optional()
+ .default("internal")
+ .describe(
+ "Play Console release track (default: internal). Use internal for testing, production for public release.",
+ ),
+};
+
+export function register(server: McpServer): void {
+ server.tool(
+ "mobile_submitToPlayStore",
+ "Submit the latest Android production build to Google Play Console via EAS Submit. Validates that EAS CLI is available and app.json has an Android package.",
+ inputSchema,
+ async (args) => {
+ try {
+ const root = args.project_path || process.cwd();
+ const appJsonPath = join(root, "app.json");
+
+ if (!existsSync(appJsonPath)) {
+ return errorResponse(
+ new Error(`No app.json at ${root}. Is this an Expo project?`),
+ );
+ }
+
+ const appJson = JSON.parse(readFileSync(appJsonPath, "utf-8"));
+ const androidPackage = appJson.expo?.android?.package;
+ if (!androidPackage) {
+ return textResponse(
+ JSON.stringify(
+ {
+ success: false,
+ message:
+ "Missing expo.android.package in app.json. Set it before submitting.",
+ example: "com.example.myapp",
+ },
+ null,
+ 2,
+ ),
+ );
+ }
+
+ try {
+ execSync("npx eas-cli --version", {
+ encoding: "utf-8",
+ stdio: ["pipe", "pipe", "pipe"],
+ timeout: 15000,
+ });
+ } catch {
+ return textResponse(
+ JSON.stringify(
+ {
+ success: false,
+ message:
+ "EAS CLI not found. Install with: npm install -g eas-cli",
+ },
+ null,
+ 2,
+ ),
+ );
+ }
+
+ const cmd = `npx eas-cli submit --platform android --non-interactive`;
+
+ try {
+ const output = execSync(cmd, {
+ cwd: root,
+ encoding: "utf-8",
+ timeout: 300000,
+ stdio: ["pipe", "pipe", "pipe"],
+ });
+
+ return textResponse(
+ JSON.stringify(
+ {
+ success: true,
+ message: `Build submitted to Google Play Console (${args.track} track)`,
+ package: androidPackage,
+ track: args.track,
+ output: output.slice(-1000),
+ next_steps: [
+ `Check Play Console for the build on the ${args.track} track`,
+ "Add release notes in Play Console",
+ args.track === "internal"
+ ? "Share the internal test link with testers"
+ : "Monitor the staged rollout in Play Console",
+ "If on internal/alpha/beta, promote to production when ready",
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ return textResponse(
+ JSON.stringify(
+ {
+ success: false,
+ message: "Submission failed",
+ error: message.slice(-1000),
+ troubleshooting: [
+ "Ensure you have a recent production build: eas build --platform android --profile production",
+ "Check Play Console service account: eas credentials --platform android",
+ "Verify the service account JSON key has Play Console API access",
+ "The app must be created in Play Console before first submission",
+ "First submission requires manual upload via Play Console",
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+ }
+ } catch (err) {
+ return errorResponse(err);
+ }
+ },
+ );
+}
diff --git a/packages/mobile-dev-tools/package.json b/packages/mobile-dev-tools/package.json
index 43aa5f2..3e64b01 100644
--- a/packages/mobile-dev-tools/package.json
+++ b/packages/mobile-dev-tools/package.json
@@ -1,6 +1,6 @@
{
"name": "@tmhs/mobile-dev-tools",
- "version": "0.6.0",
+ "version": "0.7.0",
"description": "CLI and utilities for mobile app development - environment checks, project scaffolding, store metadata validation.",
"type": "module",
"main": "dist/index.js",
diff --git a/packages/mobile-dev-tools/src/index.ts b/packages/mobile-dev-tools/src/index.ts
index e6a3ef4..ae3edba 100644
--- a/packages/mobile-dev-tools/src/index.ts
+++ b/packages/mobile-dev-tools/src/index.ts
@@ -1,3 +1,3 @@
-export const VERSION = "0.6.0";
+export const VERSION = "0.7.0";
export const PACKAGE_NAME = "@tmhs/mobile-dev-tools";
diff --git a/rules/mobile-bundle-size.mdc b/rules/mobile-bundle-size.mdc
new file mode 100644
index 0000000..995fb35
--- /dev/null
+++ b/rules/mobile-bundle-size.mdc
@@ -0,0 +1,129 @@
+---
+description: Flag large dependencies, unoptimized imports, and bloated assets that inflate the app bundle. Catches full-library imports when tree-shakeable alternatives exist, heavy packages with lighter replacements, and oversized embedded data files.
+alwaysApply: false
+globs:
+ - "*.ts"
+ - "*.tsx"
+ - "*.json"
+ - "*.dart"
+---
+
+# Bundle Size
+
+When reviewing or writing code in a React Native/Expo or Flutter project, flag these bundle size issues:
+
+## Patterns to Flag
+
+### 1. Full lodash import
+
+Flag importing the entire lodash library instead of individual functions:
+
+```tsx
+// BAD: imports the entire 70KB+ library
+import _ from "lodash";
+import { debounce } from "lodash";
+
+// GOOD: import only what you need (2-5KB each)
+import debounce from "lodash/debounce";
+import groupBy from "lodash/groupBy";
+
+// BETTER: use lodash-es for proper tree shaking
+import { debounce } from "lodash-es";
+```
+
+### 2. moment.js (suggest lighter alternatives)
+
+Flag any import of `moment` or `moment-timezone`. The library is 300KB+ with locale data.
+
+```tsx
+// BAD: 300KB+ gzipped
+import moment from "moment";
+
+// GOOD: date-fns (tree-shakeable, ~2KB per function)
+import { format, parseISO } from "date-fns";
+
+// GOOD: dayjs (2KB, moment-compatible API)
+import dayjs from "dayjs";
+```
+
+### 3. Full AWS SDK import
+
+Flag importing the entire AWS SDK v2 or the full v3 client package:
+
+```tsx
+// BAD: imports the entire AWS SDK (>50MB uncompressed)
+import AWS from "aws-sdk";
+
+// GOOD: import only the client you need
+import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
+```
+
+### 4. Large embedded JSON or data files
+
+Flag JSON files larger than 100KB imported directly in source code:
+
+```tsx
+// BAD: 500KB JSON embedded in the JS bundle
+import cities from "./data/all-cities.json";
+
+// GOOD: load from API at runtime or use expo-asset
+import { Asset } from "expo-asset";
+const asset = Asset.fromModule(require("./data/all-cities.json"));
+await asset.downloadAsync();
+```
+
+### 5. Unoptimized icon library imports
+
+Flag importing the entire icon set instead of specific icons:
+
+```tsx
+// BAD: imports all 3,000+ icons
+import * as Icons from "@expo/vector-icons";
+import { Ionicons } from "@expo/vector-icons";
+
+// GOOD: Expo vector icons are tree-shaken at build time,
+// but avoid importing * as Icons which bypasses tree shaking
+```
+
+For react-native-vector-icons (non-Expo), each icon family adds 1-2MB. Only include families you use.
+
+### 6. Duplicate functionality packages
+
+Flag when multiple packages serve the same purpose:
+
+- `axios` + `fetch` (use fetch, it is built-in)
+- `moment` + `date-fns` (pick one)
+- `lodash` + `underscore` (pick one)
+- `redux` + `zustand` + `jotai` (pick one state manager)
+
+### 7. Dev dependencies in production bundle
+
+Flag dev-only packages imported in production code:
+
+```tsx
+// BAD: storybook or testing libs imported in app code
+import { storiesOf } from "@storybook/react-native";
+import { render } from "@testing-library/react-native";
+```
+
+### 8. Flutter: unnecessary plugin dependencies
+
+Flag Flutter packages that pull in heavy native SDKs when lighter alternatives exist:
+
+```yaml
+# BAD: full Google Maps SDK (~15MB) for simple location display
+google_maps_flutter: ^2.0.0
+
+# CONSIDER: if you only need geocoding or a static map
+geocoding: ^2.0.0
+```
+
+## Recommendations
+
+When flagging a bundle size issue, suggest:
+
+1. **Measure first**: Run `npx expo export` and check the output bundle size, or use `mobile_analyzeBundle`
+2. **Tree shaking**: Use named imports from ES module packages
+3. **Lazy loading**: Use `React.lazy()` and `Suspense` for screens not needed at startup
+4. **Asset optimization**: Use WebP for images, compress fonts, externalize large data files
+5. **Dependency audit**: Run `npx depcheck` to find unused dependencies
diff --git a/skills/mobile-analytics/SKILL.md b/skills/mobile-analytics/SKILL.md
new file mode 100644
index 0000000..e316c83
--- /dev/null
+++ b/skills/mobile-analytics/SKILL.md
@@ -0,0 +1,248 @@
+---
+name: mobile-analytics
+description: Add crash reporting and event tracking to a React Native/Expo or Flutter app. Covers Sentry, Firebase Crashlytics, PostHog, source map upload, user identification, session recording, and GDPR compliance. Use when the user wants visibility into crashes, user behavior, or app performance in production.
+---
+
+# Mobile Analytics
+
+## Trigger
+
+Use this skill when the user:
+
+- Wants crash reporting or error tracking in production
+- Needs event analytics or user behavior tracking
+- Asks about Sentry, Firebase Crashlytics, PostHog, or Mixpanel
+- Wants to understand how users interact with their app
+- Mentions "analytics", "crash reporting", "crashlytics", "sentry", "tracking", "events", or "session recording"
+
+## Required Inputs
+
+- **Analytics type**: crash reporting only, event tracking only, or both
+- **Provider**: Sentry (recommended for crash + performance), Firebase Crashlytics, PostHog, or Mixpanel
+- **Framework**: Expo (React Native) or Flutter
+
+## Workflow
+
+1. **Choose a provider.** Each has different strengths:
+
+ | Provider | Best for | Free tier | Source maps | Session recording |
+ |---|---|---|---|---|
+ | Sentry | Crashes + performance monitoring | 5K errors/month | Yes | Yes (beta) |
+ | Firebase Crashlytics | Crash-only, Google ecosystem | Unlimited | Yes (via CLI) | No |
+ | PostHog | Product analytics + feature flags | 1M events/month | No | Yes |
+ | Mixpanel | Event funnels and retention | 20M events/month | No | No |
+
+ You can combine providers. A common pattern is Sentry for crashes + PostHog for product analytics.
+
+2. **Set up Sentry (recommended).** For Expo:
+
+ ```bash
+ npx expo install @sentry/react-native
+ ```
+
+ Add the config plugin in `app.json`:
+
+ ```json
+ {
+ "expo": {
+ "plugins": [
+ [
+ "@sentry/react-native/expo",
+ {
+ "organization": "your-org",
+ "project": "your-project"
+ }
+ ]
+ ]
+ }
+ }
+ ```
+
+3. **Initialize Sentry.** In `app/_layout.tsx` or your entry point:
+
+ ```tsx
+ import * as Sentry from "@sentry/react-native";
+
+ Sentry.init({
+ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN!,
+ environment: __DEV__ ? "development" : "production",
+ tracesSampleRate: __DEV__ ? 1.0 : 0.2,
+ enabled: !__DEV__,
+ });
+ ```
+
+ Wrap your root component:
+
+ ```tsx
+ export default Sentry.wrap(function RootLayout() {
+ return ;
+ });
+ ```
+
+4. **Upload source maps for readable stack traces.** With EAS Build, add a post-publish hook in `app.json`:
+
+ ```json
+ {
+ "expo": {
+ "hooks": {
+ "postPublish": [
+ {
+ "file": "@sentry/react-native/expo",
+ "config": {
+ "organization": "your-org",
+ "project": "your-project"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+ Set the auth token in your EAS secrets:
+
+ ```bash
+ eas secret:create --name SENTRY_AUTH_TOKEN --value "your-token" --scope project
+ ```
+
+5. **Track custom events.** Beyond automatic crash reporting:
+
+ ```tsx
+ import * as Sentry from "@sentry/react-native";
+
+ Sentry.addBreadcrumb({
+ category: "navigation",
+ message: "User opened profile screen",
+ level: "info",
+ });
+
+ Sentry.captureMessage("User completed onboarding");
+
+ Sentry.captureException(new Error("Payment failed"), {
+ tags: { payment_provider: "stripe" },
+ extra: { amount: 999, currency: "usd" },
+ });
+ ```
+
+6. **Set up PostHog for product analytics (optional).** Install:
+
+ ```bash
+ npx expo install posthog-react-native
+ ```
+
+ Initialize in your app:
+
+ ```tsx
+ import { PostHogProvider } from "posthog-react-native";
+
+ export default function RootLayout() {
+ return (
+
+
+
+ );
+ }
+ ```
+
+ Track events:
+
+ ```tsx
+ import { usePostHog } from "posthog-react-native";
+
+ function CheckoutScreen() {
+ const posthog = usePostHog();
+
+ const handlePurchase = () => {
+ posthog.capture("purchase_completed", {
+ product_id: "premium_yearly",
+ price: 39.99,
+ });
+ };
+ }
+ ```
+
+7. **Identify users.** Link analytics to authenticated users:
+
+ ```tsx
+ // Sentry
+ Sentry.setUser({ id: user.id, email: user.email });
+
+ // PostHog
+ posthog.identify(user.id, { email: user.email, plan: "premium" });
+
+ // On sign-out, clear the user
+ Sentry.setUser(null);
+ posthog.reset();
+ ```
+
+8. **GDPR and privacy compliance.** Respect user privacy:
+
+ ```tsx
+ import * as Sentry from "@sentry/react-native";
+
+ function disableTracking() {
+ Sentry.init({ enabled: false });
+ posthog.optOut();
+ }
+
+ function enableTracking() {
+ Sentry.init({ enabled: true, dsn: process.env.EXPO_PUBLIC_SENTRY_DSN! });
+ posthog.optIn();
+ }
+ ```
+
+ Add a privacy settings screen where users can opt out of analytics. Required for EU users under GDPR and recommended for App Store review.
+
+## Key References
+
+- [Sentry: React Native](https://docs.sentry.io/platforms/react-native/)
+- [Sentry: Expo integration](https://docs.sentry.io/platforms/react-native/manual-setup/expo/)
+- [Firebase Crashlytics: React Native](https://rnfirebase.io/crashlytics/usage)
+- [PostHog: React Native](https://posthog.com/docs/libraries/react-native)
+- [Mixpanel: React Native](https://docs.mixpanel.com/docs/tracking-methods/sdks/react-native)
+
+## Example Interaction
+
+**User:** "I want crash reporting in production and event tracking for my checkout flow."
+
+**Agent:**
+1. Recommends Sentry for crashes + PostHog for event analytics
+2. Installs @sentry/react-native and posthog-react-native with `mobile_installDependency`
+3. Configures Sentry plugin in app.json with org and project
+4. Initializes Sentry in `_layout.tsx` with production-only mode and 20% trace sampling
+5. Sets up source map uploads via EAS Build hooks
+6. Wraps app in PostHogProvider
+7. Adds purchase event tracking in checkout flow: `posthog.capture("purchase_completed", {...})`
+8. Implements user identification on login and reset on logout
+9. Reminds user to set `EXPO_PUBLIC_SENTRY_DSN` and `EXPO_PUBLIC_POSTHOG_KEY` in `.env`
+
+## MCP Usage
+
+| Step | MCP Tool | Description |
+|------|----------|-------------|
+| Install SDKs | `mobile_installDependency` | Install @sentry/react-native, posthog-react-native |
+| Check build | `mobile_checkBuildHealth` | Verify project builds with native Sentry module |
+| Validate config | `mobile_checkBuildHealth` | Ensure app.json plugins are correctly configured |
+| Build for testing | `mobile_buildForStore` | Create a production build to verify source map upload |
+
+## Common Pitfalls
+
+1. **Leaving analytics enabled in development** - Set `enabled: !__DEV__` to avoid polluting production data with dev crashes and test events.
+2. **Missing source maps** - Without source maps, crash stack traces show minified code. Configure the Sentry post-publish hook and set the auth token in EAS secrets.
+3. **Over-sampling performance traces** - A `tracesSampleRate` of 1.0 in production generates too much data and slows the app. Use 0.1-0.2 for production.
+4. **Not identifying users** - Anonymous crash reports are hard to debug. Call `Sentry.setUser()` after authentication to associate crashes with user accounts.
+5. **Tracking PII in events** - Do not log emails, passwords, or payment details in event properties. Sentry and PostHog both have data scrubbing options, but prevention is better.
+6. **Ignoring session recording consent** - Session recording captures screen content. Require explicit user consent before enabling it, especially for EU users.
+7. **Firebase Crashlytics requires a dev build** - Like most native modules, Crashlytics does not work in Expo Go.
+
+## See Also
+
+- [Mobile Monetization](../mobile-monetization/SKILL.md) - track purchase and subscription events
+- [Mobile OTA Updates](../mobile-ota-updates/SKILL.md) - track update adoption and crash rates per release
+- [Mobile Auth Setup](../mobile-auth-setup/SKILL.md) - identify users for analytics after authentication
diff --git a/skills/mobile-deep-links/SKILL.md b/skills/mobile-deep-links/SKILL.md
new file mode 100644
index 0000000..fa78e66
--- /dev/null
+++ b/skills/mobile-deep-links/SKILL.md
@@ -0,0 +1,224 @@
+---
+name: mobile-deep-links
+description: Set up universal links (iOS), app links (Android), URL schemes, and deferred deep links in a React Native/Expo or Flutter app. Covers AASA hosting, assetlinks.json, Expo Linking API, link-to-screen routing, and install attribution. Use when the user wants URLs to open specific screens in their app.
+---
+
+# Mobile Deep Links
+
+## Trigger
+
+Use this skill when the user:
+
+- Wants URLs to open specific screens in their app
+- Needs universal links (iOS) or app links (Android)
+- Asks about URL schemes, deferred deep links, or install attribution
+- Wants to share content links that open the app (or the store if not installed)
+- Mentions "deep link", "universal link", "app link", "URL scheme", "expo-linking", or "branch.io"
+
+## Required Inputs
+
+- **Link type**: URL scheme only, universal/app links, or deferred deep links
+- **Domain** (for universal/app links): the domain that will host the association files
+- **Routes**: which URL paths map to which screens
+
+## Workflow
+
+1. **Understand the three link types.**
+
+ | Type | Format | Opens app if installed | Fallback | Install attribution |
+ |---|---|---|---|---|
+ | URL scheme | `myapp://chat/123` | Yes | No (fails silently) | No |
+ | Universal/app link | `https://example.com/chat/123` | Yes | Opens in browser | No |
+ | Deferred deep link | `https://example.com/chat/123` | Yes | Store, then routes after install | Yes |
+
+ URL schemes are simplest but least reliable. Universal/app links are recommended for production. Deferred deep links require a service like Branch or Expo's built-in linking.
+
+2. **Configure URL scheme in app.json.** This is the baseline:
+
+ ```json
+ {
+ "expo": {
+ "scheme": "myapp"
+ }
+ }
+ ```
+
+ Use `mobile_configureDeepLinks` to automate this step.
+
+3. **Set up universal links (iOS).** Create an Apple App Site Association (AASA) file and host it at `https://example.com/.well-known/apple-app-site-association`:
+
+ ```json
+ {
+ "applinks": {
+ "apps": [],
+ "details": [
+ {
+ "appIDs": ["TEAMID.com.example.myapp"],
+ "components": [
+ { "/": "/chat/*", "comment": "Chat screens" },
+ { "/": "/profile/*", "comment": "Profile screens" }
+ ]
+ }
+ ]
+ }
+ }
+ ```
+
+ Add associated domains in `app.json`:
+
+ ```json
+ {
+ "expo": {
+ "ios": {
+ "associatedDomains": ["applinks:example.com"]
+ }
+ }
+ }
+ ```
+
+4. **Set up app links (Android).** Create `assetlinks.json` and host it at `https://example.com/.well-known/assetlinks.json`:
+
+ ```json
+ [
+ {
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "com.example.myapp",
+ "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
+ }
+ }
+ ]
+ ```
+
+ Get your SHA256 fingerprint from the Play Console or with:
+
+ ```bash
+ keytool -list -v -keystore your-keystore.jks
+ ```
+
+ Add intent filters in `app.json`:
+
+ ```json
+ {
+ "expo": {
+ "android": {
+ "intentFilters": [
+ {
+ "action": "VIEW",
+ "autoVerify": true,
+ "data": [
+ { "scheme": "https", "host": "example.com", "pathPrefix": "/chat" },
+ { "scheme": "https", "host": "example.com", "pathPrefix": "/profile" }
+ ],
+ "category": ["BROWSABLE", "DEFAULT"]
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+5. **Handle incoming links in your app.** Use Expo Linking API:
+
+ ```tsx
+ import { useEffect } from "react";
+ import * as Linking from "expo-linking";
+ import { useRouter } from "expo-router";
+
+ export function useDeepLinkHandler() {
+ const router = useRouter();
+
+ useEffect(() => {
+ const handleUrl = (event: { url: string }) => {
+ const parsed = Linking.parse(event.url);
+ if (parsed.path) {
+ router.push(parsed.path as any);
+ }
+ };
+
+ const subscription = Linking.addEventListener("url", handleUrl);
+
+ Linking.getInitialURL().then((url) => {
+ if (url) handleUrl({ url });
+ });
+
+ return () => subscription.remove();
+ }, []);
+ }
+ ```
+
+ With Expo Router, file-based routing handles most deep links automatically if your URL paths match your file structure.
+
+6. **Test deep links.** During development:
+
+ ```bash
+ # URL scheme (iOS Simulator)
+ xcrun simctl openurl booted "myapp://chat/123"
+
+ # URL scheme (Android Emulator)
+ adb shell am start -a android.intent.action.VIEW -d "myapp://chat/123"
+
+ # Universal link (iOS - requires dev build + AASA)
+ xcrun simctl openurl booted "https://example.com/chat/123"
+ ```
+
+ Use `npx uri-scheme` for quick testing:
+
+ ```bash
+ npx uri-scheme open "myapp://chat/123" --ios
+ npx uri-scheme open "myapp://chat/123" --android
+ ```
+
+7. **Deferred deep links (optional).** For install attribution (user clicks a link, installs the app, then lands on the right screen), use Branch or a custom solution:
+
+ ```bash
+ npx expo install react-native-branch
+ ```
+
+ Branch tracks the link pre-install and delivers the deep link data on first open after install. This is critical for marketing campaigns and referral programs.
+
+## Key References
+
+- [Expo Linking docs](https://docs.expo.dev/guides/linking/)
+- [Expo Router: Linking](https://docs.expo.dev/router/reference/url/)
+- [Apple: Supporting universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)
+- [Android: App Links](https://developer.android.com/training/app-links)
+- [AASA validator](https://branch.io/resources/aasa-validator/)
+
+## Example Interaction
+
+**User:** "I want shared recipe links like example.com/recipe/42 to open the recipe screen in my app."
+
+**Agent:**
+1. Configures URL scheme and associated domains with `mobile_configureDeepLinks`
+2. Creates AASA file template for `example.com` with `/recipe/*` path
+3. Creates Android intent filter for `https://example.com/recipe/*`
+4. Implements deep link handler using Expo Linking API
+5. Ensures Expo Router file structure matches: `app/recipe/[id].tsx`
+6. Shows how to test with `xcrun simctl openurl` and `adb shell am start`
+7. Reminds user to host AASA and assetlinks.json on their domain
+
+## MCP Usage
+
+| Step | MCP Tool | Description |
+|------|----------|-------------|
+| Configure scheme and domains | `mobile_configureDeepLinks` | Set URL scheme, intent filters, associated domains, generate AASA template |
+| Add permissions | `mobile_addPermission` | Add any required permissions for link handling |
+| Create screen | `mobile_generateScreen` | Scaffold the target screen for deep link routes |
+| Check build | `mobile_checkBuildHealth` | Verify project builds with deep link config |
+
+## Common Pitfalls
+
+1. **AASA not served correctly** - The file must be served at `/.well-known/apple-app-site-association` with `Content-Type: application/json`. No redirects. HTTPS only. No file extension.
+2. **Android autoVerify failing** - The `assetlinks.json` must contain the correct SHA256 fingerprint for your signing key. EAS builds use a different key than local debug builds.
+3. **Testing universal links in Safari** - Typing a URL directly in Safari does not trigger universal links. You must tap a link from another app (Messages, Notes, or a web page).
+4. **Expo Go limitations** - Custom URL schemes and universal links require a development build. Expo Go uses its own `exp://` scheme.
+5. **Path matching too broad** - Do not set `"/"` as your only path. This captures all links to your domain and breaks normal web browsing.
+6. **Not handling cold start** - `Linking.getInitialURL()` returns the URL that launched the app from a killed state. If you only listen for the `url` event, cold-start deep links are missed.
+
+## See Also
+
+- [Mobile Push Notifications](../mobile-push-notifications/SKILL.md) - notification tap deep linking
+- [Mobile Navigation Setup](../mobile-navigation-setup/SKILL.md) - Expo Router file-based routing for deep links
+- [Mobile Analytics](../mobile-analytics/SKILL.md) - tracking deep link attribution and conversion
diff --git a/skills/mobile-monetization/SKILL.md b/skills/mobile-monetization/SKILL.md
new file mode 100644
index 0000000..084eda0
--- /dev/null
+++ b/skills/mobile-monetization/SKILL.md
@@ -0,0 +1,222 @@
+---
+name: mobile-monetization
+description: Add in-app purchases, subscriptions, or one-time payments to a React Native/Expo or Flutter app. Covers RevenueCat, StoreKit 2, Google Play Billing, receipt validation, sandbox testing, and subscription lifecycle. Use when the user wants to charge money inside their app.
+---
+
+# Mobile Monetization
+
+## Trigger
+
+Use this skill when the user:
+
+- Wants to add in-app purchases or subscriptions
+- Asks about RevenueCat, StoreKit 2, or Google Play Billing
+- Needs help with receipt validation or sandbox testing
+- Wants to offer a paywall, freemium model, or premium features
+- Mentions "monetization", "IAP", "in-app purchase", "subscription", "paywall", or "RevenueCat"
+
+## Required Inputs
+
+- **Monetization model**: subscriptions, one-time purchase, consumables, or hybrid
+- **Products**: list of product IDs and price tiers
+- **Framework**: Expo (React Native) or Flutter
+
+## Workflow
+
+1. **Choose a monetization SDK.** RevenueCat is recommended for most apps:
+
+ | Option | Best for | Pricing |
+ |---|---|---|
+ | RevenueCat | Cross-platform, analytics, paywalls | Free up to $2.5K MTR, then 1% |
+ | Adapty | A/B testing paywalls, higher free tier | Free up to $5K MTR, then 0.6% |
+ | expo-in-app-purchases | Simple Expo-only use cases | Free (Expo SDK) |
+ | react-native-iap | Direct StoreKit 2 / Play Billing | Free (community) |
+
+2. **Install RevenueCat.** For Expo:
+
+ ```bash
+ npx expo install react-native-purchases
+ ```
+
+ For Flutter:
+
+ ```bash
+ flutter pub add purchases_flutter
+ ```
+
+ Both require a development build (not Expo Go).
+
+3. **Create a RevenueCat account and project.** At https://app.revenuecat.com:
+
+ - Create a new project
+ - Add your iOS app with the App Store Connect shared secret
+ - Add your Android app with the Play Console service account JSON
+ - Create "Entitlements" (e.g. `premium`) and attach product IDs
+ - Create "Offerings" to group products into a paywall
+
+4. **Configure products in the stores.**
+
+ **App Store Connect:**
+ - Go to Monetization > Subscriptions (or In-App Purchases)
+ - Create a subscription group and add products (monthly, yearly)
+ - Set pricing for each region
+ - Submit for review (products must be reviewed separately from the app)
+
+ **Google Play Console:**
+ - Go to Monetize > Products > Subscriptions
+ - Create base plans with pricing
+ - Activate the products
+
+5. **Initialize RevenueCat in your app.** Create `lib/purchases.ts`:
+
+ ```tsx
+ import Purchases from "react-native-purchases";
+ import { Platform } from "react-native";
+
+ const API_KEYS = {
+ ios: process.env.EXPO_PUBLIC_RC_IOS_KEY!,
+ android: process.env.EXPO_PUBLIC_RC_ANDROID_KEY!,
+ };
+
+ export async function initPurchases(userId?: string) {
+ const key = Platform.select(API_KEYS)!;
+ Purchases.configure({ apiKey: key, appUserID: userId });
+ }
+
+ export async function getOfferings() {
+ const offerings = await Purchases.getOfferings();
+ return offerings.current;
+ }
+
+ export async function purchasePackage(pkg: any) {
+ const { customerInfo } = await Purchases.purchasePackage(pkg);
+ return customerInfo.entitlements.active;
+ }
+
+ export async function checkEntitlement(id: string): Promise {
+ const info = await Purchases.getCustomerInfo();
+ return info.entitlements.active[id] !== undefined;
+ }
+
+ export async function restorePurchases() {
+ const info = await Purchases.restorePurchases();
+ return info.entitlements.active;
+ }
+ ```
+
+6. **Build a paywall screen.** Display offerings to the user:
+
+ ```tsx
+ import { useEffect, useState } from "react";
+ import { View, Text, Pressable, ActivityIndicator } from "react-native";
+ import { getOfferings, purchasePackage } from "@/lib/purchases";
+
+ export default function PaywallScreen() {
+ const [offerings, setOfferings] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ getOfferings().then((o) => {
+ setOfferings(o);
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) return ;
+ if (!offerings) return No offerings available;
+
+ return (
+
+ Upgrade to Premium
+ {offerings.availablePackages.map((pkg: any) => (
+ purchasePackage(pkg)}
+ >
+ {pkg.product.title}
+ {pkg.product.priceString}/
+ {pkg.packageType === "MONTHLY" ? "month" : "year"}
+
+ ))}
+
+ );
+ }
+ ```
+
+7. **Gate features on entitlements.** Check access before showing premium content:
+
+ ```tsx
+ import { useEffect, useState } from "react";
+ import { checkEntitlement } from "@/lib/purchases";
+
+ export function usePremium() {
+ const [isPremium, setIsPremium] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ checkEntitlement("premium").then((active) => {
+ setIsPremium(active);
+ setLoading(false);
+ });
+ }, []);
+
+ return { isPremium, loading };
+ }
+ ```
+
+8. **Handle subscription lifecycle events.** RevenueCat webhooks notify your backend of:
+
+ - Initial purchase
+ - Renewal
+ - Cancellation
+ - Billing issue (grace period)
+ - Expiration
+ - Refund
+
+ Configure webhooks in RevenueCat dashboard under Project Settings > Integrations.
+
+## Key References
+
+- [RevenueCat: React Native quickstart](https://www.revenuecat.com/docs/getting-started/installation/reactnative)
+- [RevenueCat: Flutter quickstart](https://www.revenuecat.com/docs/getting-started/installation/flutter)
+- [Expo in-app purchases docs](https://docs.expo.dev/versions/latest/sdk/in-app-purchases/)
+- [App Store Review Guidelines: In-App Purchase](https://developer.apple.com/app-store/review/guidelines/#in-app-purchase)
+- [Google Play Billing](https://developer.android.com/google/play/billing)
+
+## Example Interaction
+
+**User:** "I want to add a monthly and yearly subscription with a free trial."
+
+**Agent:**
+1. Recommends RevenueCat for cross-platform subscription management
+2. Installs react-native-purchases with `mobile_installDependency`
+3. Creates `lib/purchases.ts` with initialization, offering fetch, and purchase logic
+4. Scaffolds a paywall screen showing monthly ($4.99) and yearly ($39.99) options
+5. Implements `usePremium` hook to gate features on the "premium" entitlement
+6. Walks through App Store Connect and Play Console product creation
+7. Reminds user to set `EXPO_PUBLIC_RC_IOS_KEY` and `EXPO_PUBLIC_RC_ANDROID_KEY` in `.env`
+
+## MCP Usage
+
+| Step | MCP Tool | Description |
+|------|----------|-------------|
+| Install SDK | `mobile_installDependency` | Install react-native-purchases or purchases_flutter |
+| Create paywall screen | `mobile_generateScreen` | Scaffold paywall with offerings display |
+| Check build | `mobile_checkBuildHealth` | Verify project builds with native billing modules |
+| Validate store listing | `mobile_validateStoreMetadata` | Check app.json has required fields before submission |
+
+## Common Pitfalls
+
+1. **Testing with real money** - Use sandbox accounts (App Store Connect > Users and Access > Sandbox Testers) and Google Play test tracks. Never test purchases with real cards during development.
+2. **Apple requires the Restore button** - Apps with subscriptions or non-consumables must include a "Restore Purchases" button. Apple rejects apps without one.
+3. **Forgetting server-side validation** - RevenueCat handles receipt validation automatically. If using raw StoreKit/Play Billing, validate receipts on your server to prevent fraud.
+4. **Pricing localization** - App Store and Play Store handle currency conversion automatically. Do not hardcode prices. Always read `priceString` from the product object.
+5. **Subscription status caching** - Check entitlement status on every app launch, not just after purchase. Users can cancel or get refunded between sessions.
+6. **Not handling billing errors** - Grace periods, payment failures, and expired cards need UI messaging. RevenueCat provides `BILLING_ERROR` events for this.
+7. **Expo Go incompatibility** - Native billing modules require a development build. They will crash in Expo Go.
+
+## See Also
+
+- [Mobile App Store Prep](../mobile-app-store-prep/SKILL.md) - store listing requirements for paid apps
+- [Mobile Auth Setup](../mobile-auth-setup/SKILL.md) - associate purchases with authenticated users
+- [Mobile Analytics](../mobile-analytics/SKILL.md) - track conversion rates and revenue metrics
diff --git a/skills/mobile-ota-updates/SKILL.md b/skills/mobile-ota-updates/SKILL.md
new file mode 100644
index 0000000..19c48d3
--- /dev/null
+++ b/skills/mobile-ota-updates/SKILL.md
@@ -0,0 +1,186 @@
+---
+name: mobile-ota-updates
+description: Deploy over-the-air JavaScript updates to a React Native/Expo app using EAS Update. Covers channels, runtime versions, staged rollouts, rollback, bandwidth management, and testing published updates. For Flutter, covers Shorebird. Use when the user wants to push fixes without a full app store release.
+---
+
+# Mobile OTA Updates
+
+## Trigger
+
+Use this skill when the user:
+
+- Wants to push bug fixes or content changes without a store release
+- Asks about EAS Update, CodePush, or Shorebird
+- Needs staged rollouts or rollback for updates
+- Wants to manage multiple update channels (production, staging, preview)
+- Mentions "OTA", "over-the-air", "eas update", "hot update", "code push", or "shorebird"
+
+## Required Inputs
+
+- **Framework**: Expo (React Native) or Flutter
+- **Channel strategy**: single channel or multi-channel (production, staging)
+- **Runtime version policy**: appVersion, nativeVersion, or fingerprint (Expo)
+
+## Workflow
+
+1. **Understand what OTA can and cannot update.**
+
+ | Can update (JS/assets) | Cannot update (requires new binary) |
+ |---|---|
+ | Bug fixes in TypeScript/JavaScript | New native modules (e.g. adding expo-camera) |
+ | UI changes, styling | app.json config plugin changes |
+ | Business logic | Expo SDK version upgrades |
+ | Images, fonts, JSON data | Android/iOS permission additions |
+ | Navigation structure | Native code changes (Swift/Kotlin) |
+
+ If the change touches native code, you must submit a new binary through the app stores.
+
+2. **Configure EAS Update in app.json.** Use `mobile_configureOTA` to automate this:
+
+ ```json
+ {
+ "expo": {
+ "runtimeVersion": {
+ "policy": "fingerprint"
+ },
+ "updates": {
+ "url": "https://u.expo.dev/YOUR_PROJECT_ID",
+ "enabled": true,
+ "fallbackToCacheTimeout": 0,
+ "checkAutomatically": "ON_LOAD"
+ }
+ }
+ }
+ ```
+
+ Runtime version policies:
+ - `fingerprint` (recommended): auto-generated hash of native config. Updates are only delivered to compatible binaries.
+ - `appVersion`: uses `expo.version`. You manually control compatibility.
+ - `nativeVersion`: uses `expo.ios.buildNumber` / `expo.android.versionCode`.
+
+3. **Set up channels in eas.json.** Each build profile targets a channel:
+
+ ```json
+ {
+ "build": {
+ "production": {
+ "channel": "production"
+ },
+ "preview": {
+ "channel": "preview",
+ "distribution": "internal"
+ }
+ }
+ }
+ ```
+
+4. **Publish an update.** After making a JS-only change:
+
+ ```bash
+ eas update --channel production --message "Fix checkout crash on Android"
+ ```
+
+ For staged rollouts, limit the percentage of users who receive the update:
+
+ ```bash
+ eas update --channel production --message "New onboarding flow" --rollout-percentage 10
+ ```
+
+ Increase the rollout after monitoring:
+
+ ```bash
+ eas update:rollout --channel production --percentage 50
+ eas update:rollout --channel production --percentage 100
+ ```
+
+5. **Roll back a bad update.** If an update causes issues, republish the previous known-good bundle:
+
+ ```bash
+ # List recent updates
+ eas update:list --channel production
+
+ # Rollback to the previous update
+ eas update:rollback --channel production
+ ```
+
+ The rollback takes effect on the next app launch (users must close and reopen).
+
+6. **Test updates before production.** Publish to a preview channel first:
+
+ ```bash
+ eas update --channel preview --message "Testing new feature"
+ ```
+
+ Open the update in a development build using Expo Orbit or by scanning the QR code from the EAS dashboard. Force-close and reopen the app twice to ensure the update loads.
+
+7. **Monitor update adoption.** In the EAS dashboard, check:
+
+ - How many users are on the latest update
+ - Error rates per update (combine with Sentry release tracking)
+ - Bandwidth consumption (users download the full JS bundle on each update)
+
+8. **Flutter: Shorebird (alternative).** For Flutter apps, Shorebird provides OTA updates:
+
+ ```bash
+ # Install Shorebird CLI
+ curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
+
+ # Initialize in your Flutter project
+ shorebird init
+
+ # Create a release build
+ shorebird release android
+ shorebird release ios
+
+ # Push a patch (OTA update)
+ shorebird patch android
+ shorebird patch ios
+ ```
+
+ Shorebird patches Dart code at the AOT level, not JavaScript. Limitations are similar: no native code changes, no new plugins.
+
+## Key References
+
+- [EAS Update: Getting started](https://docs.expo.dev/eas-update/getting-started/)
+- [EAS Update: Best practices](https://expo.dev/blog/eas-update-best-practices)
+- [EAS Update: Rollouts](https://docs.expo.dev/eas-update/rollouts/)
+- [EAS Update: Runtime versions](https://docs.expo.dev/eas-update/runtime-versions/)
+- [Shorebird: Getting started](https://docs.shorebird.dev/)
+
+## Example Interaction
+
+**User:** "I found a crash in production and need to push a fix without going through app review."
+
+**Agent:**
+1. Confirms the fix is JS-only (no native code changes)
+2. Verifies EAS Update is configured with `mobile_configureOTA`
+3. Walks through fixing the bug locally
+4. Publishes to the preview channel for testing: `eas update --channel preview`
+5. After testing, publishes to production at 10% rollout: `eas update --channel production --rollout-percentage 10`
+6. Monitors crash rates via Sentry, then increases to 100%
+7. Explains that users get the fix on next app launch without a store update
+
+## MCP Usage
+
+| Step | MCP Tool | Description |
+|------|----------|-------------|
+| Configure OTA | `mobile_configureOTA` | Set up EAS Update config in app.json with channels and runtime version |
+| Check build health | `mobile_checkBuildHealth` | Verify app.json has valid update config |
+| Build baseline | `mobile_buildForStore` | Create the initial binary that OTA updates will target |
+| Analyze impact | `mobile_analyzeBundle` | Check bundle size before publishing (affects download bandwidth) |
+
+## Common Pitfalls
+
+1. **Pushing native changes via OTA** - If you added a new native module or changed a config plugin, the update will crash on incompatible binaries. The `fingerprint` runtime version policy prevents this automatically.
+2. **Not testing updates** - Always publish to a preview channel first. A broken OTA update affects all users immediately (unlike store releases, which can take days to propagate).
+3. **Ignoring bandwidth** - Each OTA update downloads the full JS bundle. For apps with frequent updates and large bundles, this adds up. Use `mobile_analyzeBundle` to minimize bundle size.
+4. **Forgetting fallbackToCacheTimeout** - Setting this to `0` means the app loads the cached bundle immediately and downloads updates in the background. Setting it higher blocks launch until the update downloads, which hurts perceived performance.
+5. **Runtime version mismatch** - If you change the runtime version policy mid-project, existing users may stop receiving updates. Stick with one policy.
+6. **Rollback delay** - Rollbacks take effect on the next app launch. Users who have already loaded the bad update will keep it until they restart the app.
+7. **EAS Update requires EAS Build** - Updates are tied to builds created with EAS Build. Locally built binaries (expo run:ios) do not receive EAS Updates.
+
+## See Also
+
+- [Mobile Analytics](../mobile-analytics/SKILL.md) - track crash rates per update release
+- [Mobile iOS Submission](../mobile-ios-submission/SKILL.md) - when OTA is not enough and a store release is needed
+- [Mobile Android Submission](../mobile-android-submission/SKILL.md) - Play Store releases for native changes