diff --git a/.cursor/commands/onboard-new-developer.md b/.cursor/commands/onboard-new-developer.md index 38622e2c..0a6e27c4 100644 --- a/.cursor/commands/onboard-new-developer.md +++ b/.cursor/commands/onboard-new-developer.md @@ -1,4 +1,9 @@ Comprehensive onboarding process to get new developer up and running quickly. Follow architecture, strategies, and decisions in `@apps/docu/`. After implementation, update docs, readme, and cursor rules if required to keep them aligned. 1. **Environment setup**: Install required tools, set up development environment, configure IDE/extensions, set up git/SSH keys -2. **Project familiarization**: Review project structure, understand architecture, read key documentation, set up local database, verify all tests passing, can run application locally, submit first PR +2. **Clone and setup**: Run `pnpm setup` at repo root +3. **Decide context**: Local only, or remote VPC? (Remote VPC = dev machine in cloud; trade-offs: networking, access, cost, latency. See @apps/docu/content/docs/development/dev-environments.) +4. **Run web/API**: `pnpm dev` (API + Next.js) +5. **Run mobile (optional)**: `pnpm --filter @repo/mobile start` (or `start:localhost` / `start:tunnel` for remote). See @apps/docu/content/docs/development/dev-environments +6. **Verify**: Simulator/device can load the app and reach the API +7. **Project familiarization**: Review project structure, understand architecture, read key documentation, set up local database, verify all tests passing, submit first PR diff --git a/.cursor/rules/frontend/design.mdc b/.cursor/rules/frontend/design.mdc index a4871a1a..75ec0f30 100644 --- a/.cursor/rules/frontend/design.mdc +++ b/.cursor/rules/frontend/design.mdc @@ -1,5 +1,5 @@ --- -glob: "**/*.{tsx,css}" +glob: "apps/web/**/*.{tsx,css}" alwaysApply: true --- diff --git a/.cursor/rules/frontend/expo.mdc b/.cursor/rules/frontend/expo.mdc new file mode 100644 index 00000000..1912c71c --- /dev/null +++ b/.cursor/rules/frontend/expo.mdc @@ -0,0 +1,31 @@ +--- +description: Expo/React Native standards +glob: "apps/mobile/**/*" +--- + +## Expo Router +File-based routing: `src/app/`, `_layout.tsx`, route groups `(group)`. `_layout` cannot be DOM component—see @.cursor/skills/expo-use-dom + +## NativeTabs (SDK 55+) +Use `NativeTabs.Trigger.Icon`, `NativeTabs.Trigger.Label`, `NativeTabs.Trigger.Badge` (not separate imports). See @.cursor/skills/expo-upgrading/references/native-tabs.md + +## React Native +Primitives only: View, Text, Pressable, ScrollView—no HTML elements + +## Styling +Tailwind via NativeWind v5 + react-native-css. Use `className` on View, Text, Pressable, etc. StyleSheet for complex cases. See @.cursor/skills/expo-tailwind-setup-v55 + +## Platform extensions +Use `.web.tsx`, `.native.tsx` for platform-specific code + +## Path aliases +`@/*` → `src/`, `@/assets/*` → `assets/` + +## Hooks +Kebab-case filenames, collocated in `hooks/`. See @.cursor/rules/frontend/react-hooks.mdc + +## React Compiler +Enabled in app.json—no manual `useMemo`/`useCallback`. Aligns with @.cursor/rules/frontend/react-hooks.mdc + +## EAS / CI +EAS builds: mobile-build (manual), mobile-preview (main), mobile-pr-preview (EAS Update). EXPO_TOKEN required. See @apps/docu/content/docs/deployment/mobile-cicd.mdx diff --git a/.cursor/rules/frontend/mobile-first.mdc b/.cursor/rules/frontend/mobile-first.mdc index bcb7c29c..ff09851d 100644 --- a/.cursor/rules/frontend/mobile-first.mdc +++ b/.cursor/rules/frontend/mobile-first.mdc @@ -1,6 +1,6 @@ --- -description: Mobile-first responsive design patterns and best practices -glob: "**/*.{tsx,css}" +description: Mobile-first responsive design patterns and best practices (Tailwind breakpoints) +glob: "apps/web/**/*.{tsx,css}" alwaysApply: true --- diff --git a/.cursor/rules/frontend/nextjs.mdc b/.cursor/rules/frontend/nextjs.mdc index 1991fb77..ec85efeb 100644 --- a/.cursor/rules/frontend/nextjs.mdc +++ b/.cursor/rules/frontend/nextjs.mdc @@ -1,6 +1,6 @@ --- description: Next.js standards -glob: "**/*.{tsx,css}" +glob: "apps/web/**/*.{tsx,css}" alwaysApply: true --- diff --git a/.cursor/rules/frontend/shadcnui.mdc b/.cursor/rules/frontend/shadcnui.mdc index 7368bcf1..819e2c75 100644 --- a/.cursor/rules/frontend/shadcnui.mdc +++ b/.cursor/rules/frontend/shadcnui.mdc @@ -1,6 +1,6 @@ --- description: ShadcnUI component guidelines and patterns -glob: "**/*.{tsx,css}" +glob: "apps/web/**/*.{tsx,css},packages/ui/**/*.{tsx,css}" alwaysApply: true --- diff --git a/.cursor/skills/expo-cicd-workflows-v55/SKILL.md b/.cursor/skills/expo-cicd-workflows-v55/SKILL.md new file mode 100644 index 00000000..17f10315 --- /dev/null +++ b/.cursor/skills/expo-cicd-workflows-v55/SKILL.md @@ -0,0 +1,92 @@ +--- +name: expo-cicd-workflows-v55 +description: Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation. +allowed-tools: "Read,Write,Bash(node:*)" +version: 1.0.0 +license: MIT License +--- + +# EAS Workflows Skill + +Help developers write and edit EAS CI/CD workflow YAML files. + +## Reference Documentation + +Fetch these resources before generating or validating workflow files. Use the fetch script (implemented using Node.js) in this skill's `scripts/` directory; it caches responses using ETags for efficiency: + +```bash +# Fetch resources +node {baseDir}/scripts/fetch.js +``` + +1. **JSON Schema** — https://api.expo.dev/v2/workflows/schema + - It is NECESSARY to fetch this schema + - Source of truth for validation + - All job types and their required/optional parameters + - Trigger types and configurations + - Runner types, VM images, and all enums + +2. **Syntax Documentation** — https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/syntax.mdx + - Overview of workflow YAML syntax + - Examples and English explanations + - Expression syntax and contexts + +3. **Pre-packaged Jobs** — https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/pre-packaged-jobs.mdx + - Documentation for supported pre-packaged job types + - Job-specific parameters and outputs + +Do not rely on memorized values; these resources evolve as new features are added. + +## Workflow File Location + +Workflows live in `.eas/workflows/*.yml` (or `.yaml`). + +## Top-Level Structure + +A workflow file has these top-level keys: + +- `name` — Display name for the workflow +- `on` — Triggers that start the workflow (at least one required) +- `jobs` — Job definitions (required) +- `defaults` — Shared defaults for all jobs +- `concurrency` — Control parallel workflow runs + +Consult the schema for the full specification of each section. + +## Expressions + +Use `${{ }}` syntax for dynamic values. The schema defines available contexts: + +- `github.*` — GitHub repository and event information +- `inputs.*` — Values from `workflow_dispatch` inputs +- `needs.*` — Outputs and status from dependent jobs +- `jobs.*` — Job outputs (alternative syntax) +- `steps.*` — Step outputs within custom jobs +- `workflow.*` — Workflow metadata + +## Generating Workflows + +When generating or editing workflows: + +1. Fetch the schema to get current job types, parameters, and allowed values +2. Validate that required fields are present for each job type +3. Verify job references in `needs` and `after` exist in the workflow +4. Check that expressions reference valid contexts and outputs +5. Ensure `if` conditions respect the schema's length constraints + +## Validation + +After generating or editing a workflow file, validate it against the schema: + +```sh +# Install dependencies if missing +[ -d "{baseDir}/scripts/node_modules" ] || npm install --prefix {baseDir}/scripts + +node {baseDir}/scripts/validate.js [workflow2.yml ...] +``` + +The validator fetches the latest schema and checks the YAML structure. Fix any reported errors before considering the workflow complete. + +## Answering Questions + +When users ask about available options (job types, triggers, runner types, etc.), fetch the schema and derive the answer from it rather than relying on potentially outdated information. diff --git a/.cursor/skills/expo-cicd-workflows-v55/scripts/fetch.js b/.cursor/skills/expo-cicd-workflows-v55/scripts/fetch.js new file mode 100644 index 00000000..2a4e08da --- /dev/null +++ b/.cursor/skills/expo-cicd-workflows-v55/scripts/fetch.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import process from 'node:process' + +const CACHE_DIRECTORY = resolve(import.meta.dirname, '.cache') +const DEFAULT_TTL_SECONDS = 15 * 60 // 15 minutes + +export async function fetchCached(url) { + await mkdir(CACHE_DIRECTORY, { recursive: true }) + + const cacheFile = resolve(CACHE_DIRECTORY, `${hashUrl(url)}.json`) + const cached = await loadCacheEntry(cacheFile) + if (cached && cached.expires > Math.floor(Date.now() / 1000)) { + return cached.data + } + + // Make request, with conditional If-None-Match if we have an ETag. + // Cache-Control: max-age=0 overrides Node's default 'no-cache' to allow 304 responses. + const response = await fetch(url, { + signal: AbortSignal.timeout(30_000), + headers: { + 'Cache-Control': 'max-age=0', + ...(cached?.etag && { 'If-None-Match': cached.etag }), + }, + }) + + if (response.status === 304 && cached) { + // Refresh expiration and return cached data + const entry = { ...cached, expires: getExpires(response.headers) } + await saveCacheEntry(cacheFile, entry) + return cached.data + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const etag = response.headers.get('etag') + const data = await response.text() + const expires = getExpires(response.headers) + + await saveCacheEntry(cacheFile, { url, etag, expires, data }) + + return data +} + +function hashUrl(url) { + return createHash('sha256').update(url).digest('hex').slice(0, 16) +} + +async function loadCacheEntry(cacheFile) { + try { + return JSON.parse(await readFile(cacheFile, 'utf-8')) + } catch { + return null + } +} + +async function saveCacheEntry(cacheFile, entry) { + await writeFile(cacheFile, JSON.stringify(entry, null, 2)) +} + +function getExpires(headers) { + const now = Math.floor(Date.now() / 1000) + + // Prefer Cache-Control: max-age + const maxAgeSeconds = parseMaxAge(headers.get('cache-control')) + if (maxAgeSeconds != null) { + return now + maxAgeSeconds + } + + // Fall back to Expires header + const expires = headers.get('expires') + if (expires) { + const expiresTime = Date.parse(expires) + if (!Number.isNaN(expiresTime)) { + return Math.floor(expiresTime / 1000) + } + } + + // Default TTL + return now + DEFAULT_TTL_SECONDS +} + +function parseMaxAge(cacheControl) { + if (!cacheControl) { + return null + } + const match = cacheControl.match(/max-age=(\d+)/i) + return match ? parseInt(match[1], 10) : null +} + +if (import.meta.main) { + const url = process.argv[2] + + if (!url || url === '--help' || url === '-h') { + console.log(`Usage: fetch + +Fetches a URL with HTTP caching (ETags + Cache-Control/Expires). +Default TTL: ${DEFAULT_TTL_SECONDS / 60} minutes. +Cache is stored in: ${CACHE_DIRECTORY}/`) + process.exit(url ? 0 : 1) + } + + const data = await fetchCached(url) + console.log(data) +} diff --git a/.cursor/skills/expo-cicd-workflows-v55/scripts/package.json b/.cursor/skills/expo-cicd-workflows-v55/scripts/package.json new file mode 100644 index 00000000..a3bd7168 --- /dev/null +++ b/.cursor/skills/expo-cicd-workflows-v55/scripts/package.json @@ -0,0 +1,11 @@ +{ + "name": "@expo/cicd-workflows-skill", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "js-yaml": "^4.1.0" + } +} diff --git a/.cursor/skills/expo-cicd-workflows-v55/scripts/validate.js b/.cursor/skills/expo-cicd-workflows-v55/scripts/validate.js new file mode 100644 index 00000000..a003ed6a --- /dev/null +++ b/.cursor/skills/expo-cicd-workflows-v55/scripts/validate.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import process from 'node:process' + +import Ajv2020 from 'ajv/dist/2020.js' +import addFormats from 'ajv-formats' +import yaml from 'js-yaml' + +import { fetchCached } from './fetch.js' + +const SCHEMA_URL = 'https://api.expo.dev/v2/workflows/schema' + +async function fetchSchema() { + const data = await fetchCached(SCHEMA_URL) + const body = JSON.parse(data) + return body.data +} + +function createValidator(schema) { + const ajv = new Ajv2020({ allErrors: true, strict: true }) + addFormats(ajv) + return ajv.compile(schema) +} + +async function validateFile(validator, filePath) { + let content + try { + content = await readFile(filePath, 'utf-8') + } catch (e) { + return { valid: false, error: `Unable to read file: ${e.message}` } + } + + let doc + try { + doc = yaml.load(content) + } catch (e) { + return { valid: false, error: `YAML parse error: ${e.message}` } + } + + const valid = validator(doc) + if (!valid) { + return { valid: false, error: formatErrors(validator.errors) } + } + + return { valid: true } +} + +function formatErrors(errors) { + return errors + .map(error => { + const path = error.instancePath || '(root)' + const allowed = error.params?.allowedValues?.join(', ') + return ` ${path}: ${error.message}${allowed ? ` (allowed: ${allowed})` : ''}` + }) + .join('\n') +} + +if (import.meta.main) { + const args = process.argv.slice(2) + const files = args.filter(a => !a.startsWith('-')) + + if (files.length === 0 || args.includes('--help') || args.includes('-h')) { + console.log(`Usage: validate [workflow2.yml ...] + +Validates EAS workflow YAML files against the official schema.`) + process.exit(files.length === 0 ? 1 : 0) + } + + let validator + try { + validator = createValidator(await fetchSchema()) + } catch (e) { + console.error(`✗ Failed to load workflow schema\n${e.message}`) + process.exit(1) + } + + let hasErrors = false + + for (const file of files) { + const filePath = resolve(process.cwd(), file) + const result = await validateFile(validator, filePath) + + if (result.valid) { + console.log(`✓ ${file}`) + } else { + console.error(`✗ ${file}\n${result.error}`) + hasErrors = true + } + } + + process.exit(hasErrors ? 1 : 0) +} diff --git a/.cursor/skills/expo-deployment-v55/SKILL.md b/.cursor/skills/expo-deployment-v55/SKILL.md new file mode 100644 index 00000000..cc580631 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/SKILL.md @@ -0,0 +1,190 @@ +--- +name: expo-deployment-v55 +description: Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes +version: 1.0.0 +license: MIT +--- + +# Deployment + +This skill covers deploying Expo applications across all platforms using EAS (Expo Application Services). + +## References + +Consult these resources as needed: + +- ./references/workflows.md -- CI/CD workflows for automated deployments and PR previews +- ./references/testflight.md -- Submitting iOS builds to TestFlight for beta testing +- ./references/app-store-metadata.md -- Managing App Store metadata and ASO optimization +- ./references/play-store.md -- Submitting Android builds to Google Play Store +- ./references/ios-app-store.md -- iOS App Store submission and review process + +## Quick Start + +### Install EAS CLI + +```bash +npm install -g eas-cli +eas login +``` + +### Initialize EAS + +```bash +npx eas-cli@latest init +``` + +This creates `eas.json` with build profiles. + +## Build Commands + +### Production Builds + +```bash +# iOS App Store build +npx eas-cli@latest build -p ios --profile production + +# Android Play Store build +npx eas-cli@latest build -p android --profile production + +# Both platforms +npx eas-cli@latest build --profile production +``` + +### Submit to Stores + +```bash +# iOS: Build and submit to App Store Connect +npx eas-cli@latest build -p ios --profile production --auto-submit + +# Android: Build and submit to Play Store +npx eas-cli@latest build -p android --profile production --auto-submit + +# Submit latest iOS build to TestFlight +eas submit -p ios --latest +``` + +## Web Deployment + +Deploy web apps using EAS Hosting: + +```bash +# Deploy to production +npx expo export -p web +npx eas-cli@latest deploy --prod + +# Deploy PR preview +npx eas-cli@latest deploy +``` + +## EAS Configuration + +Standard `eas.json` for production deployments: + +```json +{ + "cli": { + "version": ">= 18.3.0", + "appVersionSource": "remote" + }, + "build": { + "production": { + "autoIncrement": true, + "ios": { + "resourceClass": "m-medium" + } + }, + "development": { + "developmentClient": true, + "distribution": "internal" + } + }, + "submit": { + "production": { + "ios": { + "appleId": "your@email.com", + "ascAppId": "1234567890" + }, + "android": { + "serviceAccountKeyPath": "./google-service-account.json", + "track": "internal" + } + } + } +} +``` + +## Platform-Specific Guides + +### iOS + +- Use `npx testflight` for quick TestFlight submissions +- Configure Apple credentials via `eas credentials` +- See ./references/testflight.md for credential setup +- See ./references/ios-app-store.md for App Store submission + +### Android + +- Set up Google Play Console service account +- Configure tracks: internal → closed → open → production +- See ./references/play-store.md for detailed setup + +### Web + +- EAS Hosting provides preview URLs for PRs +- Production deploys to your custom domain +- See ./references/workflows.md for CI/CD automation + +## Automated Deployments + +Use EAS Workflows for CI/CD: + +```yaml +# .eas/workflows/release.yml +name: Release + +on: + push: + branches: [main] + +jobs: + build-ios: + type: build + params: + platform: ios + profile: production + + submit-ios: + type: submit + needs: [build-ios] + params: + build_id: ${{ needs.build-ios.outputs.build_id }} + profile: production +``` + +See ./references/workflows.md for more workflow examples. + +## Version Management + +EAS manages version numbers automatically with `appVersionSource: "remote"`: + +```bash +# Check current versions +eas build:version:get + +# Manually set version +eas build:version:set -p ios --build-number 42 +``` + +## Monitoring + +```bash +# List recent builds +eas build:list + +# Check build status +eas build:view + +# View submission status +eas submit:list +``` diff --git a/.cursor/skills/expo-deployment-v55/references/app-store-metadata.md b/.cursor/skills/expo-deployment-v55/references/app-store-metadata.md new file mode 100644 index 00000000..32861dd3 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/references/app-store-metadata.md @@ -0,0 +1,491 @@ +# App Store Metadata + +Manage App Store metadata and optimize for ASO using EAS Metadata. + +## What is EAS Metadata? + +EAS Metadata automates App Store presence management from the command line using a `store.config.json` file instead of manually filling forms in App Store Connect. It includes built-in validation to catch common rejection pitfalls. + +**Current Status:** Preview, Apple App Store only. + +## Getting Started + +### Pull Existing Metadata + +If your app is already published, pull current metadata: + +```bash +eas metadata:pull +``` + +This creates `store.config.json` with your current App Store configuration. + +### Push Metadata Updates + +After editing your config, push changes: + +```bash +eas metadata:push +``` + +**Important:** You must submit a binary via `eas submit` before pushing metadata for new apps. + +## Configuration File + +Create `store.config.json` at your project root: + +```json +{ + "configVersion": 0, + "apple": { + "copyright": "2025 Your Company", + "categories": ["UTILITIES", "PRODUCTIVITY"], + "info": { + "en-US": { + "title": "App Name", + "subtitle": "Your compelling tagline", + "description": "Full app description...", + "keywords": ["keyword1", "keyword2", "keyword3"], + "releaseNotes": "What's new in this version...", + "promoText": "Limited time offer!", + "privacyPolicyUrl": "https://example.com/privacy", + "supportUrl": "https://example.com/support", + "marketingUrl": "https://example.com" + } + }, + "advisory": { + "alcoholTobaccoOrDrugUseOrReferences": "NONE", + "gamblingSimulated": "NONE", + "medicalOrTreatmentInformation": "NONE", + "profanityOrCrudeHumor": "NONE", + "sexualContentGraphicAndNudity": "NONE", + "sexualContentOrNudity": "NONE", + "horrorOrFearThemes": "NONE", + "matureOrSuggestiveThemes": "NONE", + "violenceCartoonOrFantasy": "NONE", + "violenceRealistic": "NONE", + "violenceRealisticProlongedGraphicOrSadistic": "NONE", + "contests": "NONE", + "gambling": false, + "unrestrictedWebAccess": false, + "seventeenPlus": false + }, + "release": { + "automaticRelease": true, + "phasedRelease": true + }, + "review": { + "firstName": "John", + "lastName": "Doe", + "email": "review@example.com", + "phone": "+1 555-123-4567", + "notes": "Demo account: test@example.com / password123" + } + } +} +``` + +## App Store Optimization (ASO) + +### Title Optimization (30 characters max) + +The title is the most important ranking factor. Include your brand name and 1-2 strongest keywords. + +```json +{ + "title": "Budgetly - Money Tracker" +} +``` + +**Best Practices:** + +- Brand name first for recognition +- Include highest-volume keyword +- Avoid generic words like "app" or "the" +- Title keywords boost rankings by ~10% + +### Subtitle Optimization (30 characters max) + +The subtitle appears below your title in search results. Use it for your unique value proposition. + +```json +{ + "subtitle": "Smart Expense & Budget Planner" +} +``` + +**Best Practices:** + +- Don't duplicate keywords from title (Apple counts each word once) +- Highlight your main differentiator +- Include secondary high-value keywords +- Focus on benefits, not features + +### Keywords Field (100 characters max) + +Hidden from users but crucial for discoverability. Use comma-separated keywords without spaces after commas. + +```json +{ + "keywords": [ + "finance", + "budget", + "expense", + "money", + "tracker", + "savings", + "bills", + "income", + "spending", + "wallet", + "personal", + "weekly", + "monthly" + ] +} +``` + +**Best Practices:** + +- Use all 100 characters +- Separate with commas only (no spaces) +- No duplicates from title/subtitle +- Include singular forms (Apple handles plurals) +- Add synonyms and alternate spellings +- Include competitor brand names (carefully) +- Use digits instead of spelled numbers ("5" not "five") +- Skip articles and prepositions + +### Description Optimization + +The iOS description is NOT indexed for search but critical for conversion. Focus on convincing users to download. + +```json +{ + "description": "Take control of your finances with Budgetly, the intuitive money management app trusted by over 1 million users.\n\nKEY FEATURES:\n• Smart budget tracking - Set limits and watch your progress\n• Expense categorization - Know exactly where your money goes\n• Bill reminders - Never miss a payment\n• Beautiful charts - Visualize your financial health\n• Bank sync - Connect 10,000+ institutions\n• Cloud backup - Your data, always safe\n\nWHY BUDGETLY?\nUnlike complex spreadsheets or basic calculators, Budgetly learns your spending habits and provides personalized insights. Our users save an average of $300/month within 3 months.\n\nPRIVACY FIRST\nYour financial data is encrypted end-to-end. We never sell your information.\n\nDownload Budgetly today and start your journey to financial freedom!" +} +``` + +**Best Practices:** + +- Front-load the first 3 lines (visible before "more") +- Use bullet points for features +- Include social proof (user counts, ratings, awards) +- Add a clear call-to-action +- Mention privacy/security for sensitive apps +- Update with each release + +### Release Notes + +Shown to existing users deciding whether to update. + +```json +{ + "releaseNotes": "Version 2.5 brings exciting improvements:\n\n• NEW: Dark mode support\n• NEW: Widget for home screen\n• IMPROVED: 50% faster sync\n• FIXED: Notification timing issues\n\nLove Budgetly? Please leave a review!" +} +``` + +### Promo Text (170 characters max) + +Appears above description; can be updated without new binary. Great for time-sensitive promotions. + +```json +{ + "promoText": "🎉 New Year Special: Premium features free for 30 days! Start 2025 with better finances." +} +``` + +## Categories + +Primary category is most important for browsing and rankings. + +```json +{ + "categories": ["FINANCE", "PRODUCTIVITY"] +} +``` + +**Available Categories:** + +- BOOKS, BUSINESS, DEVELOPER_TOOLS, EDUCATION +- ENTERTAINMENT, FINANCE, FOOD_AND_DRINK +- GAMES (with subcategories), GRAPHICS_AND_DESIGN +- HEALTH_AND_FITNESS, KIDS (age-gated) +- LIFESTYLE, MAGAZINES_AND_NEWSPAPERS +- MEDICAL, MUSIC, NAVIGATION, NEWS +- PHOTO_AND_VIDEO, PRODUCTIVITY, REFERENCE +- SHOPPING, SOCIAL_NETWORKING, SPORTS +- STICKERS (with subcategories), TRAVEL +- UTILITIES, WEATHER + +## Localization + +Localize metadata for each target market. Keywords should be researched per locale—direct translations often miss regional search terms. + +```json +{ + "info": { + "en-US": { + "title": "Budgetly - Money Tracker", + "subtitle": "Smart Expense Planner", + "keywords": ["budget", "finance", "money", "expense", "tracker"] + }, + "es-ES": { + "title": "Budgetly - Control de Gastos", + "subtitle": "Planificador de Presupuesto", + "keywords": ["presupuesto", "finanzas", "dinero", "gastos", "ahorro"] + }, + "ja": { + "title": "Budgetly - 家計簿アプリ", + "subtitle": "簡単支出管理", + "keywords": ["家計簿", "支出", "予算", "節約", "お金"] + }, + "de-DE": { + "title": "Budgetly - Haushaltsbuch", + "subtitle": "Ausgaben Verwalten", + "keywords": ["budget", "finanzen", "geld", "ausgaben", "sparen"] + } + } +} +``` + +**Supported Locales:** +`ar-SA`, `ca`, `cs`, `da`, `de-DE`, `el`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `fi`, `fr-CA`, `fr-FR`, `he`, `hi`, `hr`, `hu`, `id`, `it`, `ja`, `ko`, `ms`, `nl-NL`, `no`, `pl`, `pt-BR`, `pt-PT`, `ro`, `ru`, `sk`, `sv`, `th`, `tr`, `uk`, `vi`, `zh-Hans`, `zh-Hant` + +## Dynamic Configuration + +Use JavaScript for dynamic values like copyright year or fetched translations. + +### Basic Dynamic Config + +```js +// store.config.js +const baseConfig = require("./store.config.json"); + +const year = new Date().getFullYear(); + +module.exports = { + ...baseConfig, + apple: { + ...baseConfig.apple, + copyright: `${year} Your Company, Inc.`, + }, +}; +``` + +### Async Configuration (External Localization) + +```js +// store.config.js +module.exports = async () => { + const baseConfig = require("./store.config.json"); + + // Fetch translations from CMS/localization service + const translations = await fetch( + "https://api.example.com/app-store-copy" + ).then((r) => r.json()); + + return { + ...baseConfig, + apple: { + ...baseConfig.apple, + info: translations, + }, + }; +}; +``` + +### Environment-Based Config + +```js +// store.config.js +const baseConfig = require("./store.config.json"); + +const isProduction = process.env.EAS_BUILD_PROFILE === "production"; + +module.exports = { + ...baseConfig, + apple: { + ...baseConfig.apple, + info: { + "en-US": { + ...baseConfig.apple.info["en-US"], + promoText: isProduction + ? "Download now and get started!" + : "[BETA] Help us test new features!", + }, + }, + }, +}; +``` + +Update `eas.json` to use JS config: + +```json +{ + "cli": { + "metadataPath": "./store.config.js" + } +} +``` + +## Age Rating (Advisory) + +Answer content questions honestly to get an appropriate age rating. + +**Content Descriptors:** + +- `NONE` - Content not present +- `INFREQUENT_OR_MILD` - Occasional mild content +- `FREQUENT_OR_INTENSE` - Regular or strong content + +```json +{ + "advisory": { + "alcoholTobaccoOrDrugUseOrReferences": "NONE", + "contests": "NONE", + "gambling": false, + "gamblingSimulated": "NONE", + "horrorOrFearThemes": "NONE", + "matureOrSuggestiveThemes": "NONE", + "medicalOrTreatmentInformation": "NONE", + "profanityOrCrudeHumor": "NONE", + "sexualContentGraphicAndNudity": "NONE", + "sexualContentOrNudity": "NONE", + "unrestrictedWebAccess": false, + "violenceCartoonOrFantasy": "NONE", + "violenceRealistic": "NONE", + "violenceRealisticProlongedGraphicOrSadistic": "NONE", + "seventeenPlus": false, + "kidsAgeBand": "NINE_TO_ELEVEN" + } +} +``` + +**Kids Age Bands:** `FIVE_AND_UNDER`, `SIX_TO_EIGHT`, `NINE_TO_ELEVEN` + +## Release Strategy + +Control how your app rolls out to users. + +```json +{ + "release": { + "automaticRelease": true, + "phasedRelease": true + } +} +``` + +**Options:** + +- `automaticRelease: true` - Release immediately upon approval +- `automaticRelease: false` - Manual release after approval +- `automaticRelease: "2025-02-01T10:00:00Z"` - Schedule release (RFC 3339) +- `phasedRelease: true` - 7-day gradual rollout (1%, 2%, 5%, 10%, 20%, 50%, 100%) + +## Review Information + +Provide contact info and test credentials for the App Review team. + +```json +{ + "review": { + "firstName": "Jane", + "lastName": "Smith", + "email": "app-review@company.com", + "phone": "+1 (555) 123-4567", + "demoUsername": "demo@example.com", + "demoPassword": "ReviewDemo2025!", + "notes": "To test premium features:\n1. Log in with demo credentials\n2. Navigate to Settings > Subscription\n3. Tap 'Restore Purchase' - sandbox purchase will be restored\n\nFor location features, allow location access when prompted." + } +} +``` + +## ASO Checklist + +### Before Each Release + +- [ ] Update keywords based on performance data +- [ ] Refresh description with new features +- [ ] Write compelling release notes +- [ ] Update promo text if running campaigns +- [ ] Verify all URLs are valid + +### Monthly ASO Tasks + +- [ ] Analyze keyword rankings +- [ ] Research competitor keywords +- [ ] Check conversion rates in App Analytics +- [ ] Review user feedback for keyword ideas +- [ ] A/B test screenshots in App Store Connect + +### Keyword Research Tips + +1. **Brainstorm features** - List all app capabilities +2. **Mine reviews** - Find words users actually use +3. **Analyze competitors** - Check their titles/subtitles +4. **Use long-tail keywords** - Less competition, higher intent +5. **Consider misspellings** - Common typos can drive traffic +6. **Track seasonality** - Some keywords peak at certain times + +### Metrics to Monitor + +- **Impressions** - How often your app appears in search +- **Product Page Views** - Users who tap to learn more +- **Conversion Rate** - Views → Downloads +- **Keyword Rankings** - Position for target keywords +- **Category Ranking** - Position in your categories + +## VS Code Integration + +Install the [Expo Tools extension](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) for: + +- Auto-complete for all schema properties +- Inline validation and warnings +- Quick fixes for common issues + +## Common Issues + +### "Binary not found" + +Push a binary with `eas submit` before pushing metadata. + +### "Invalid keywords" + +- Check total length is ≤100 characters +- Remove spaces after commas +- Remove duplicate words + +### "Description too long" + +Description maximum is 4000 characters. + +### Pull doesn't update JS config + +`eas metadata:pull` creates a JSON file; import it into your JS config. + +## CI/CD Integration + +Automate metadata updates in your deployment pipeline: + +```yaml +# .eas/workflows/release.yml +jobs: + submit-and-metadata: + steps: + - name: Submit to App Store + run: eas submit -p ios --latest + + - name: Push Metadata + run: eas metadata:push +``` + +## Tips + +- Update metadata every 4-6 weeks for optimal ASO +- 70% of App Store visitors use search to find apps +- Apps with 4+ star ratings get featured more often +- Localized apps see 128% more downloads per country +- First 3 lines of description are most critical (shown before "more") +- Use all 100 keyword characters—every character counts diff --git a/.cursor/skills/expo-deployment-v55/references/ios-app-store.md b/.cursor/skills/expo-deployment-v55/references/ios-app-store.md new file mode 100644 index 00000000..9dd1e357 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/references/ios-app-store.md @@ -0,0 +1,375 @@ +# Submitting to iOS App Store + +## Prerequisites + +1. **Apple Developer Account** - Enroll at [developer.apple.com](https://developer.apple.com) +2. **App Store Connect App** - Create your app record before first submission +3. **Apple Credentials** - Configure via EAS or environment variables + +## Credential Setup + +### Using EAS Credentials + +```bash +eas credentials -p ios +``` + +This interactive flow helps you: +- Create or select a distribution certificate +- Create or select a provisioning profile +- Configure App Store Connect API key (recommended) + +### App Store Connect API Key (Recommended) + +API keys avoid 2FA prompts in CI/CD: + +1. Go to App Store Connect → Users and Access → Keys +2. Click "+" to create a new key +3. Select "App Manager" role (minimum for submissions) +4. Download the `.p8` key file + +Configure in `eas.json`: + +```json +{ + "submit": { + "production": { + "ios": { + "ascAppId": "1234567890", + "ascApiKeyPath": "./AuthKey_XXXXX.p8", + "ascApiKeyIssuerId": "xxxxx-xxxx-xxxx-xxxx-xxxxx", + "ascApiKeyId": "XXXXXXXXXX" + } + } + } +} +``` + +Or use environment variables: + +```bash +EXPO_ASC_API_KEY_PATH=./AuthKey.p8 +EXPO_ASC_ISSUER_ID=xxxxx-xxxx-xxxx-xxxx-xxxxx +EXPO_ASC_KEY_ID=XXXXXXXXXX +``` + +### Apple ID Authentication (Alternative) + +For non-API-key submissions, configure `appleId` in `eas.json` and provide an app-specific password: + +```json +{ + "submit": { + "production": { + "ios": { + "ascAppId": "1234567890", + "appleId": "your@email.com" + } + } + } +} +``` + +```bash +EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx +``` + +Note: This fallback requires an app-specific password for accounts with 2FA. + +## Submission Commands + +```bash +# Build and submit to App Store Connect +eas build -p ios --profile production --auto-submit + +# Submit latest build +eas submit -p ios --latest + +# Submit specific build +eas submit -p ios --id BUILD_ID + +# Quick TestFlight submission +npx testflight +``` + +## App Store Connect Configuration + +### First-Time Setup + +Before submitting, complete in App Store Connect: + +1. **App Information** + - Primary language + - Bundle ID (must match `app.json`) + - SKU (unique identifier) + +2. **Pricing and Availability** + - Price tier + - Available countries + +3. **App Privacy** + - Privacy policy URL + - Data collection declarations + +4. **App Review Information** + - Contact information + - Demo account (if login required) + - Notes for reviewers + +### EAS Configuration + +```json +{ + "cli": { + "version": ">= 18.0.5", + "appVersionSource": "remote" + }, + "build": { + "production": { + "ios": { + "resourceClass": "m-medium", + "autoIncrement": true + } + } + }, + "submit": { + "production": { + "ios": { + "appleId": "your@email.com", + "ascAppId": "1234567890", + "appleTeamId": "XXXXXXXXXX" + } + } + } +} +``` + +Find `ascAppId` in App Store Connect → App Information → Apple ID. + +## TestFlight vs App Store + +### TestFlight (Beta Testing) + +- Builds go to TestFlight automatically after submission +- Internal testers (up to 100) - immediate access +- External testers (up to 10,000) - requires beta review +- Builds expire after 90 days + +### App Store (Production) + +- Requires passing App Review +- Submit for review from App Store Connect +- Choose release timing (immediate, scheduled, manual) + +## App Review Process + +### What Reviewers Check + +1. **Functionality** - App works as described +2. **UI/UX** - Follows Human Interface Guidelines +3. **Content** - Appropriate and accurate +4. **Privacy** - Data handling matches declarations +5. **Legal** - Complies with local laws + +### Common Rejection Reasons + +| Issue | Solution | +|-------|----------| +| Crashes/bugs | Test thoroughly before submission | +| Incomplete metadata | Fill all required fields | +| Placeholder content | Remove "lorem ipsum" and test data | +| Missing login credentials | Provide demo account | +| Privacy policy missing | Add URL in App Store Connect | +| Guideline 4.2 (minimum functionality) | Ensure app provides value | + +### Expedited Review + +Request expedited review for: +- Critical bug fixes +- Time-sensitive events +- Security issues + +Go to App Store Connect → your app → App Review → Request Expedited Review. + +## Version and Build Numbers + +iOS uses two version identifiers: + +- **Version** (`CFBundleShortVersionString`): User-facing, e.g., "1.2.3" +- **Build Number** (`CFBundleVersion`): Internal, must increment for each upload + +Configure in `app.json`: + +```json +{ + "expo": { + "version": "1.2.3", + "ios": { + "buildNumber": "1" + } + } +} +``` + +With `autoIncrement: true`, EAS handles build numbers automatically. + +## Release Options + +These `apple.release` keys belong in `store.config.json` (or the file referenced by `submit.ios.metadataPath` in `eas.json`). + +### Automatic Release + +Release immediately when approved: + +```json +{ + "configVersion": 0, + "apple": { + "release": { + "automaticRelease": true + } + } +} +``` + +### Scheduled Release + +In `store.config.json`: + +```json +{ + "configVersion": 0, + "apple": { + "release": { + "automaticRelease": "YYYY-MM-DDTHH:mm:ssZ" + } + } +} +``` + +### Phased Release + +In `store.config.json`. Gradual rollout over 7 days: + +```json +{ + "configVersion": 0, + "apple": { + "release": { + "phasedRelease": true + } + } +} +``` + +Rollout: Day 1 (1%) → Day 2 (2%) → Day 3 (5%) → Day 4 (10%) → Day 5 (20%) → Day 6 (50%) → Day 7 (100%) + +## Certificates and Provisioning + +### Distribution Certificate + +- Required for App Store submissions +- Team-level credential; only one of each distribution certificate type allowed per team +- Valid for 1 year +- EAS manages automatically + +### Provisioning Profile + +- Links app, certificate, and entitlements +- App Store profiles don't include device UDIDs +- EAS creates and manages automatically + +### Check Current Credentials + +```bash +eas credentials -p ios + +# Sync with Apple Developer Portal +eas credentials -p ios --sync +``` + +## App Store Metadata + +Use EAS Metadata to manage App Store listing from code: + +```bash +# Pull existing metadata +eas metadata:pull + +# Push changes +eas metadata:push +``` + +See @.cursor/skills/expo-deployment-v55/references/app-store-metadata.md for detailed configuration. + +## Troubleshooting + +### "No suitable application records found" + +Create the app in App Store Connect first with matching bundle ID. + +### "The bundle version must be higher" + +Increment build number. With `autoIncrement: true`, this is automatic. + +### "Missing compliance information" + +Add export compliance to `app.json`: + +```json +{ + "expo": { + "ios": { + "config": { + "usesNonExemptEncryption": false + } + } + } +} +``` + +### "Invalid provisioning profile" + +```bash +eas credentials -p ios --sync +``` + +### Build stuck in "Processing" + +App Store Connect processing can take 5-30 minutes. Check status in App Store Connect → TestFlight. + +## CI/CD Integration + +For automated submissions in CI/CD: + +```yaml +# .eas/workflows/release.yml +name: Release to App Store + +on: + push: + tags: ['v*'] + +jobs: + build: + type: build + params: + platform: ios + profile: production + + submit: + type: submit + needs: [build] + params: + build_id: ${{ needs.build.outputs.build_id }} + profile: production +``` + +## Tips + +- Submit to TestFlight early and often for feedback +- Use beta app review for external testers to catch issues before App Store review +- Respond to reviewer questions promptly in App Store Connect +- Keep demo account credentials up-to-date +- Monitor App Store Connect notifications for review updates +- Use phased release for major updates to catch issues early diff --git a/.cursor/skills/expo-deployment-v55/references/play-store.md b/.cursor/skills/expo-deployment-v55/references/play-store.md new file mode 100644 index 00000000..88102dd1 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/references/play-store.md @@ -0,0 +1,246 @@ +# Submitting to Google Play Store + +## Prerequisites + +1. **Google Play Console Account** - Register at [play.google.com/console](https://play.google.com/console) +2. **App Created in Console** - Create your app listing before first submission +3. **Service Account** - For automated submissions via EAS + +## Service Account Setup + +### 1. Create Service Account + +1. Go to Google Cloud Console → IAM & Admin → Service Accounts +2. Create a new service account +3. Grant the "Service Account User" role +4. Create and download a JSON key + +### 2. Link to Play Console + +1. Go to Play Console → Setup → API access +2. Click "Link" next to your Google Cloud project +3. Under "Service accounts", click "Manage Play Console permissions" +4. Grant "Release to production" permission (or appropriate track permissions) + +### 3. Configure EAS + +Add the service account key path to `eas.json`: + +```json +{ + "submit": { + "production": { + "android": { + "serviceAccountKeyPath": "./google-service-account.json", + "track": "internal" + } + } + } +} +``` + +Store the key file securely and add it to `.gitignore`. + +## Environment Variables + +For CI/CD, use environment variables instead of file paths: + +```bash +# Base64-encoded service account JSON +EXPO_ANDROID_SERVICE_ACCOUNT_KEY_BASE64=... +``` + +Or use EAS Secrets: + +```bash +eas secret:create --name GOOGLE_SERVICE_ACCOUNT --value "$(cat google-service-account.json)" --type file +``` + +Then reference in `eas.json`: + +```json +{ + "submit": { + "production": { + "android": { + "serviceAccountKeyPath": "@secret:GOOGLE_SERVICE_ACCOUNT" + } + } + } +} +``` + +## Release Tracks + +Google Play uses tracks for staged rollouts: + +| Track | Purpose | +|-------|---------| +| `internal` | Internal testing (up to 100 testers) | +| `alpha` | Closed testing | +| `beta` | Open testing | +| `production` | Public release | + +### Track Configuration + +```json +{ + "submit": { + "production": { + "android": { + "track": "production", + "releaseStatus": "completed" + } + }, + "internal": { + "android": { + "track": "internal", + "releaseStatus": "completed" + } + } + } +} +``` + +### Release Status Options + +- `completed` - Immediately available on the track +- `draft` - Upload only, release manually in Console +- `halted` - Pause an in-progress rollout +- `inProgress` - Staged rollout (requires `rollout` percentage) + +## Staged Rollout + +```json +{ + "submit": { + "production": { + "android": { + "track": "production", + "releaseStatus": "inProgress", + "rollout": 0.1 + } + } + } +} +``` + +This releases to 10% of users. Increase via Play Console or subsequent submissions. + +## Submission Commands + +```bash +# Build and submit to internal track +eas build -p android --profile production --submit + +# Submit existing build to Play Store +eas submit -p android --latest + +# Submit specific build +eas submit -p android --id BUILD_ID +``` + +## App Signing + +### Google Play App Signing (Recommended) + +EAS uses Google Play App Signing by default: + +1. First upload: EAS creates upload key, Play Store manages signing key +2. Play Store re-signs your app with the signing key +3. Upload key can be reset if compromised + +### Checking Signing Status + +```bash +eas credentials -p android +``` + +## Version Codes + +Android requires incrementing `versionCode` for each upload: + +```json +{ + "build": { + "production": { + "autoIncrement": true + } + } +} +``` + +With `appVersionSource: "remote"`, EAS tracks version codes automatically. + +## First Submission Checklist + +Before your first Play Store submission: + +- [ ] Create app in Google Play Console +- [ ] Complete app content declaration (privacy policy, ads, etc.) +- [ ] Set up store listing (title, description, screenshots) +- [ ] Complete content rating questionnaire +- [ ] Set up pricing and distribution +- [ ] Create service account with proper permissions +- [ ] Configure `eas.json` with service account path + +## Common Issues + +### "App not found" + +The app must exist in Play Console before EAS can submit. Create it manually first. + +### "Version code already used" + +Increment `versionCode` in `app.json` or use `autoIncrement: true` in `eas.json`. + +### "Service account lacks permission" + +Ensure the service account has "Release to production" permission in Play Console → API access. + +### "APK not acceptable" + +Play Store requires AAB (Android App Bundle) for new apps: + +```json +{ + "build": { + "production": { + "android": { + "buildType": "app-bundle" + } + } + } +} +``` + +## Internal Testing Distribution + +For quick internal distribution without Play Store: + +```bash +# Build with internal distribution +eas build -p android --profile development + +# Share the APK link with testers +``` + +Or use EAS Update for OTA updates to existing installs. + +## Monitoring Submissions + +```bash +# Check submission status +eas submit:list -p android + +# View specific submission +eas submit:view SUBMISSION_ID +``` + +## Tips + +- Start with `internal` track for testing before production +- Use staged rollouts for production releases +- Keep service account key secure - never commit to git +- Set up Play Console notifications for review status +- Pre-launch reports in Play Console catch issues before review diff --git a/.cursor/skills/expo-deployment-v55/references/testflight.md b/.cursor/skills/expo-deployment-v55/references/testflight.md new file mode 100644 index 00000000..decbc0d5 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/references/testflight.md @@ -0,0 +1,70 @@ +# TestFlight + +Always ship to TestFlight first. Internal testers, then external testers, then App Store. Never skip this. + +## Submit + +```bash +npx testflight +``` + +`npx testflight` runs the Expo TestFlight CLI (no install required). It builds and submits to TestFlight in one command. See [Expo docs](https://docs.expo.dev/build-reference/npx-testflight/) for details. + +## Skip the Prompts + +Set these once and forget: + +```bash +EXPO_APPLE_ID=you@email.com +EXPO_APPLE_TEAM_ID=XXXXXXXXXX +``` + +The CLI prints your Team ID when you run `npx testflight`. Copy it. + +## Why TestFlight First + +- Internal testers get builds instantly (no review) +- External testers require one Beta App Review, then instant updates +- Catch crashes before App Store review rejects you +- TestFlight crash reports are better than App Store crash reports +- 90 days to test before builds expire +- Real users on real devices, not simulators + +## Tester Strategy + +**Internal (100 max)**: Your team. Immediate access. Use for every build. + +**External (10,000 max)**: Beta users. First build needs review (~24h), then instant. Always have an external group—even if it's just friends. Real feedback beats assumptions. + +## Tips + +- Submit to external TestFlight the moment internal looks stable +- Beta App Review is faster and more lenient than App Store Review +- Add release notes—testers actually read them +- Use TestFlight's built-in feedback and screenshots +- Never go straight to App Store. Ever. + +## Troubleshooting + +**"No suitable application records found"** +Create the app in App Store Connect first. Bundle ID must match. + +**"The bundle version must be higher"** +Add `autoIncrement` to the build profile's `ios` section in `eas.json`: + +```json +{ + "build": { + "production": { + "ios": { + "autoIncrement": "buildNumber" + } + } + } +} +``` + +**Credentials issues** +```bash +eas credentials -p ios +``` diff --git a/.cursor/skills/expo-deployment-v55/references/workflows.md b/.cursor/skills/expo-deployment-v55/references/workflows.md new file mode 100644 index 00000000..419d6d96 --- /dev/null +++ b/.cursor/skills/expo-deployment-v55/references/workflows.md @@ -0,0 +1,220 @@ +# EAS Workflows + +Automate builds, submissions, and deployments with EAS Workflows. + +## Web Deployment + +Deploy web apps on push to main: + +`.eas/workflows/deploy.yml` + +```yaml +name: Deploy + +on: + push: + branches: + - main + +# https://docs.expo.dev/eas/workflows/syntax/#deploy +jobs: + deploy_web: + type: deploy + params: + prod: true +``` + +## PR Previews + +### Web PR Previews + +```yaml +name: Web PR Preview + +on: + pull_request: + types: [opened, synchronize] + +jobs: + preview: + type: deploy + params: + prod: false +``` + +### Native PR Previews with EAS Updates + +Deploy OTA updates for pull requests: + +```yaml +name: PR Preview + +on: + pull_request: + types: [opened, synchronize] + +jobs: + publish: + type: update + params: + branch: "pr-${{ github.event.pull_request.number }}" + message: "PR #${{ github.event.pull_request.number }}" +``` + +## Production Release + +Complete release workflow for both platforms: + +```yaml +name: Release + +on: + push: + tags: ['v*'] + +jobs: + build-ios: + type: build + params: + platform: ios + profile: production + + build-android: + type: build + params: + platform: android + profile: production + + submit-ios: + type: submit + needs: [build-ios] + params: + build_id: ${{ needs.build-ios.outputs.build_id }} + profile: production + + submit-android: + type: submit + needs: [build-android] + params: + build_id: ${{ needs.build-android.outputs.build_id }} + profile: production +``` + +## Build on Push + +Trigger builds when pushing to specific branches: + +```yaml +name: Build + +on: + push: + branches: + - main + - release/* + +jobs: + build-ios: + type: build + params: + platform: ios + profile: production + + build-android: + type: build + params: + platform: android + profile: production +``` + +## Conditional Jobs + +Run jobs based on conditions: + +```yaml +name: Conditional Release + +on: + push: + branches: [main] + +jobs: + check-changes: + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + steps: + - uses: eas/checkout + - id: check + run: | + if git diff --name-only HEAD~1 | grep -q "^src/"; then + set-output has_changes "true" + else + set-output has_changes "false" + fi + + build-ios: + type: build + needs: [check-changes] + if: needs.check-changes.outputs.has_changes == 'true' + params: + platform: ios + profile: production + + build-android: + type: build + needs: [check-changes] + if: needs.check-changes.outputs.has_changes == 'true' + params: + platform: android + profile: production +``` + +## Workflow Syntax Reference + +### Triggers + +```yaml +on: + push: + branches: [main, develop] + tags: ['v*'] + pull_request: + types: [opened, synchronize, reopened] + schedule: + - cron: '0 0 * * *' # Daily at midnight + workflow_dispatch: # Manual trigger +``` + +### Job Types + +| Type | Purpose | +|------|---------| +| `build` | Create app builds | +| `submit` | Submit to app stores | +| `update` | Publish OTA updates | +| `deploy` | Deploy web apps | +| `run` | Execute custom commands | + +### Job Dependencies + +```yaml +jobs: + first: + type: build + params: + platform: ios + + second: + type: submit + needs: [first] # Runs after 'first' completes + params: + build_id: ${{ needs.first.outputs.build_id }} + profile: production +``` + +## Tips + +- Use `workflow_dispatch` for manual production releases +- Combine PR previews with GitHub status checks +- Use tags for versioned releases +- Keep sensitive values in EAS Secrets, not workflow files diff --git a/.cursor/skills/expo-dev-client-v55/SKILL.md b/.cursor/skills/expo-dev-client-v55/SKILL.md new file mode 100644 index 00000000..74a88d7c --- /dev/null +++ b/.cursor/skills/expo-dev-client-v55/SKILL.md @@ -0,0 +1,170 @@ +--- +name: expo-dev-client-v55 +description: Build and distribute Expo development clients locally or via TestFlight +version: 1.0.0 +license: MIT +--- + +Use EAS Build to create development clients for testing native code changes on physical devices. Use this for creating custom Expo Go clients for testing branches of your app. + +## Important: When Development Clients Are Needed + +**Only create development clients when your app requires custom native code.** Most apps work fine in Expo Go. + +You need a dev client ONLY when using: +- Local Expo modules (custom native code) +- Apple targets (widgets, app clips, extensions) +- Third-party native modules not in Expo Go + +**Try Expo Go first** with `npx expo start`. If everything works, you don't need a dev client. + +## EAS Configuration + +Ensure `eas.json` has a development profile: + +```json +{ + "cli": { + "version": ">= 18.0.5", + "appVersionSource": "remote" + }, + "build": { + "production": { + "autoIncrement": true + }, + "development": { + "autoIncrement": true, + "developmentClient": true + }, + "testflight": { + "autoIncrement": true, + "developmentClient": true, + "distribution": "store" + } + }, + "submit": { + "production": {}, + "development": {}, + "testflight": {} + } +} +``` + +Key settings: +- `developmentClient: true` - Bundles expo-dev-client for development builds +- `autoIncrement: true` - Automatically increments build numbers +- `appVersionSource: "remote"` - Uses EAS as the source of truth for version numbers + +## Building for TestFlight + +Build iOS dev client and submit to TestFlight in one command (use the `testflight` profile for App Store–signed builds; `development` is for local Metro/dev-client testing): + +```bash +eas build -p ios --profile testflight --auto-submit +``` + +This will: +1. Build the development client in the cloud +2. Automatically submit to App Store Connect +3. Send you an email when the build is ready in TestFlight + +After receiving the TestFlight email: +1. Download the build from TestFlight on your device +2. Launch the app to see the expo-dev-client UI +3. Connect to your local Metro bundler or scan a QR code + +## Building Locally + +Build a development client on your machine: + +```bash +# iOS (requires Xcode) +eas build -p ios --profile development --local + +# Android +eas build -p android --profile development --local +``` + +Local builds output: +- iOS simulator: `.tar.gz` archive; iOS device (with signing): `.ipa` file +- Android: `.apk` file + +## Installing Local Builds + +Install iOS build on simulator: + +```bash +# Find the .app in the .tar.gz output +tar -xzf build-*.tar.gz +xcrun simctl install booted ./path/to/App.app +``` + +Install iOS build on device (requires signing): + +```bash +# Use Xcode Devices window or ideviceinstaller +ideviceinstaller -i build.ipa +``` + +Install Android build: + +```bash +adb install build.apk +``` + +## Building for Specific Platform + +```bash +# iOS only +eas build -p ios --profile development + +# Android only +eas build -p android --profile development + +# Both platforms +eas build --profile development +``` + +## Checking Build Status + +```bash +# List recent builds +eas build:list + +# View build details +eas build:view +``` + +## Using the Dev Client + +Once installed, the dev client provides: +- **Development server connection** - Enter your Metro bundler URL or scan QR +- **Build information** - View native build details +- **Launcher UI** - Switch between development servers + +Connect to local development: + +```bash +# Start Metro bundler +npx expo start --dev-client + +# Scan QR code with dev client or enter URL manually +``` + +## Troubleshooting + +**Build fails with signing errors:** +```bash +eas credentials +``` + +**Clear build cache:** +```bash +eas build -p ios --profile development --clear-cache +``` + +**Check EAS CLI version:** +```bash +eas --version +npm install -g eas-cli@latest +``` diff --git a/.cursor/skills/expo-tailwind-setup-v55/SKILL.md b/.cursor/skills/expo-tailwind-setup-v55/SKILL.md new file mode 100644 index 00000000..94e23e1d --- /dev/null +++ b/.cursor/skills/expo-tailwind-setup-v55/SKILL.md @@ -0,0 +1,498 @@ +--- +name: expo-tailwind-setup-v55 +description: Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling +version: 1.0.0 +license: MIT +--- + +# Tailwind CSS Setup for Expo with react-native-css + +This guide covers setting up Tailwind CSS v4 in Expo using react-native-css and NativeWind v5 for universal styling across iOS, Android, and Web. + +## Overview + +This setup uses: + +- **Tailwind CSS v4** - Modern CSS-first configuration +- **react-native-css** - CSS runtime for React Native +- **NativeWind v5** - Metro transformer for Tailwind in React Native +- **@tailwindcss/postcss** - PostCSS plugin for Tailwind v4 + +## Installation + +> **Note:** `react-native-css@0.0.0-nightly.5ce6396` and `nativewind@5.0.0-preview.2` are pre-release/experimental and may be unstable. Check for newer stable releases before using. + +```bash +# Install dependencies +npx expo install tailwindcss@^4 nativewind@5.0.0-preview.2 react-native-css@0.0.0-nightly.5ce6396 @tailwindcss/postcss tailwind-merge clsx +``` + +Add pnpm overrides for lightningcss compatibility: + +```json +// package.json +{ + "overrides": { + "lightningcss": "1.30.1" + } +} +``` + +- autoprefixer is not needed in Expo because of lightningcss +- postcss is included in expo by default + +## Configuration Files + +### Metro Config + +Create or update `metro.config.js`: + +```js +// metro.config.js +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativewind } = require("nativewind/metro"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +module.exports = withNativewind(config, { + // inline variables break PlatformColor in CSS variables + inlineVariables: false, + // We add className support manually + globalClassNamePolyfill: false, +}); +``` + +### PostCSS Config + +Create `postcss.config.mjs`: + +```js +// postcss.config.mjs +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +### Global CSS + +Create `src/global.css`: + +```css +@import "tailwindcss"; + +/* Platform-specific font families */ +@media android { + :root { + --font-mono: monospace; + --font-rounded: normal; + --font-serif: serif; + --font-sans: normal; + } +} + +@media ios { + :root { + --font-mono: ui-monospace; + --font-serif: ui-serif; + --font-sans: system-ui; + --font-rounded: ui-rounded; + } +} +``` + +> Use granular imports (`@import "tailwindcss/theme.css" layer(theme);`, etc.) only when deliberately omitting or customizing Preflight or other layers. + +## IMPORTANT: No Babel Config Needed + +With Tailwind v4 and NativeWind v5, you do NOT need a babel.config.js for Tailwind. Remove any NativeWind babel presets if present: + +```js +// DELETE babel.config.js if it only contains NativeWind config +// The following is NO LONGER needed: +// module.exports = function (api) { +// api.cache(true); +// return { +// presets: [ +// ["babel-preset-expo", { jsxImportSource: "nativewind" }], +// "nativewind/babel", +// ], +// }; +// }; +``` + +## CSS Component Wrappers + +Since react-native-css requires explicit CSS element wrapping, create reusable components: + +### Main Components (`src/tw/index.tsx`) + +```tsx +import { + useCssElement, + useNativeVariable as useFunctionalVariable, +} from "react-native-css"; + +import { Link as RouterLink } from "expo-router"; +import Animated from "react-native-reanimated"; +import React from "react"; +import { + View as RNView, + Text as RNText, + Pressable as RNPressable, + ScrollView as RNScrollView, + TouchableHighlight as RNTouchableHighlight, + TextInput as RNTextInput, + StyleSheet, +} from "react-native"; + +// CSS-enabled Link +const LinkBase = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RouterLink, props, { className: "style" }); +}; + +export const Link = Object.assign(LinkBase, { + Trigger: RouterLink.Trigger, + Menu: RouterLink.Menu, + MenuAction: RouterLink.MenuAction, + Preview: RouterLink.Preview, +}); + +// CSS Variable hook +export const useCSSVariable = + process.env.EXPO_OS !== "web" + ? useFunctionalVariable + : (variable: string) => `var(${variable})`; + +// View +export type ViewProps = React.ComponentProps & { + className?: string; +}; + +export const View = (props: ViewProps) => { + return useCssElement(RNView, props, { className: "style" }); +}; +View.displayName = "CSS(View)"; + +// Text +export const Text = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNText, props, { className: "style" }); +}; +Text.displayName = "CSS(Text)"; + +// ScrollView +export const ScrollView = ( + props: React.ComponentProps & { + className?: string; + contentContainerClassName?: string; + } +) => { + return useCssElement(RNScrollView, props, { + className: "style", + contentContainerClassName: "contentContainerStyle", + }); +}; +ScrollView.displayName = "CSS(ScrollView)"; + +// Pressable +export const Pressable = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNPressable, props, { className: "style" }); +}; +Pressable.displayName = "CSS(Pressable)"; + +// TextInput +export const TextInput = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNTextInput, props, { className: "style" }); +}; +TextInput.displayName = "CSS(TextInput)"; + +// AnimatedScrollView +export const AnimatedScrollView = ( + props: React.ComponentProps & { + className?: string; + contentClassName?: string; + contentContainerClassName?: string; + } +) => { + return useCssElement(Animated.ScrollView, props, { + className: "style", + contentClassName: "contentContainerStyle", + contentContainerClassName: "contentContainerStyle", + }); +}; + +// TouchableHighlight with underlayColor extraction +function BaseTouchableHighlight( + props: React.ComponentProps +) { + const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {}; + return ( + + ); +} + +export const TouchableHighlight = ( + props: React.ComponentProps +) => { + return useCssElement(BaseTouchableHighlight, props, { className: "style" }); +}; +TouchableHighlight.displayName = "CSS(TouchableHighlight)"; + +export { Image } from "./image"; +``` + +### Image Component (`src/tw/image.tsx`) + +```tsx +import { useCssElement } from "react-native-css"; +import React from "react"; +import { StyleSheet } from "react-native"; +import Animated from "react-native-reanimated"; +import { Image as RNImage } from "expo-image"; + +const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage); + +export type ImageProps = React.ComponentProps; + +function CSSImage(props: React.ComponentProps) { + // @ts-expect-error: Remap objectFit style to contentFit property + const { objectFit, objectPosition, ...style } = + StyleSheet.flatten(props.style) || {}; + + return ( + + ); +} + +export const Image = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(CSSImage, props, { className: "style" }); +}; + +Image.displayName = "CSS(Image)"; +``` + +### Animated Components (`src/tw/animated.tsx`) + +```tsx +import * as TW from "./index"; +import RNAnimated from "react-native-reanimated"; + +export const Animated = { + ...RNAnimated, + View: RNAnimated.createAnimatedComponent(TW.View), +}; +``` + +## Usage + +See also: @expo-deployment-v55, @expo-cicd-workflows-v55. + +Import CSS-wrapped components from your tw directory: + +```tsx +import { View, Text, ScrollView, Image } from "@/tw"; + +export default function MyScreen() { + return ( + + + Hello Tailwind! + + + + ); +} +``` + +## Custom Theme Variables + +Add custom theme variables in your global.css using `@theme`: + +```css +@layer theme { + @theme { + /* Custom fonts */ + --font-rounded: "SF Pro Rounded", sans-serif; + + /* Custom line heights */ + --text-xs--line-height: calc(1em / 0.75); + --text-sm--line-height: calc(1.25em / 0.875); + --text-base--line-height: calc(1.5em / 1); + + /* Custom leading scales */ + --leading-tight: 1.25em; + --leading-snug: 1.375em; + --leading-normal: 1.5em; + } +} +``` + +## Platform-Specific Styles + +Use platform media queries for platform-specific styling: + +```css +@media ios { + :root { + --font-sans: system-ui; + --font-rounded: ui-rounded; + } +} + +@media android { + :root { + --font-sans: normal; + --font-rounded: normal; + } +} +``` + +## Apple System Colors with CSS Variables + +Create a CSS file for Apple semantic colors: + +```css +/* src/css/sf.css */ +@layer base { + html { + color-scheme: light; + } +} + +:root { + --sf-blue: rgb(0 122 255); + --sf-green: rgb(52 199 89); + --sf-red: rgb(255 59 48); + --sf-gray: rgb(142 142 147); + --sf-gray-2: rgb(174 174 178); + --sf-text: rgb(0 0 0); + --sf-text-2: rgb(60 60 67 / 0.6); + --sf-bg: rgb(255 255 255); + --sf-bg-2: rgb(242 242 247); +} + +.dark, +[data-theme="dark"] { + --sf-blue: rgb(10 132 255); + --sf-green: rgb(48 209 89); + --sf-red: rgb(255 69 58); + --sf-gray: rgb(142 142 147); + --sf-gray-2: rgb(99 99 102); + --sf-text: rgb(255 255 255); + --sf-text-2: rgb(235 235 245 / 0.6); + --sf-bg: rgb(0 0 0); + --sf-bg-2: rgb(28 28 30); +} + +/* iOS native colors via platformColor */ +@media ios { + :root { + --sf-blue: platformColor(systemBlue); + --sf-green: platformColor(systemGreen); + --sf-red: platformColor(systemRed); + --sf-gray: platformColor(systemGray); + --sf-text: platformColor(label); + --sf-text-2: platformColor(secondaryLabel); + --sf-bg: platformColor(systemBackground); + --sf-bg-2: platformColor(secondarySystemBackground); + } +} + +/* Register as Tailwind theme colors */ +@layer theme { + @theme { + --color-sf-blue: var(--sf-blue); + --color-sf-green: var(--sf-green); + --color-sf-red: var(--sf-red); + --color-sf-gray: var(--sf-gray); + --color-sf-text: var(--sf-text); + --color-sf-text-2: var(--sf-text-2); + --color-sf-bg: var(--sf-bg); + --color-sf-bg-2: var(--sf-bg-2); + } +} +``` + +Then use in components: + +```tsx +Primary text +Secondary text +... +``` + +## Using CSS Variables in JavaScript + +Use the `useCSSVariable` hook: + +```tsx +import { useCSSVariable } from "@/tw"; + +function MyComponent() { + const blue = useCSSVariable("--sf-blue"); + + return ; +} +``` + +## Key Differences from NativeWind v4 / Tailwind v3 + +See also: @expo-deployment-v55, @expo-cicd-workflows-v55. + +1. **No babel.config.js** - Configuration is now CSS-first +2. **PostCSS plugin** - Uses `@tailwindcss/postcss` instead of `tailwindcss` +3. **CSS imports** - Use single `@import "tailwindcss";` (granular imports like `@import "tailwindcss/components";` only as an explicit exception) +4. **Theme config** - Use `@theme` in CSS instead of `tailwind.config.js` +5. **Component wrappers** - Must wrap components with `useCssElement` for className support +6. **Metro config** - Use `withNativewind` with different options (`inlineVariables: false`) + +## Troubleshooting + +See also: @expo-deployment-v55, @expo-cicd-workflows-v55. + +### Styles not applying + +1. Ensure you have the CSS file imported in your app entry +2. Check that components are wrapped with `useCssElement` +3. Verify Metro config has `withNativewind` applied + +### Platform colors not working + +1. Use `platformColor()` in `@media ios` blocks +2. For web/Android, use explicit `:root` (light) and `.dark` / `[data-theme="dark"]` (dark) overrides; react-native-css does not support `light-dark()` + +### TypeScript errors + +Add className to component props: + +```tsx +type Props = React.ComponentProps & { className?: string }; +``` diff --git a/.cursor/skills/expo-upgrading-v55/SKILL.md b/.cursor/skills/expo-upgrading-v55/SKILL.md new file mode 100644 index 00000000..b90edd4e --- /dev/null +++ b/.cursor/skills/expo-upgrading-v55/SKILL.md @@ -0,0 +1,133 @@ +--- +name: expo-upgrading-v55 +description: Guidelines for upgrading Expo SDK versions and fixing dependency issues +version: 1.0.0 +license: MIT +--- + +## References + +- ./references/new-architecture.md -- SDK +53: New Architecture migration guide +- ./references/react-19.md -- SDK +54: React 19 changes (use() optional for Context, as provider alternative, ref as prop; forwardRef deprecated) +- ./references/react-compiler.md -- SDK +54: React Compiler setup and migration guide +- ./references/native-tabs.md -- SDK +55: Native tabs changes (Icon/Label/Badge now accessed via NativeTabs.Trigger.\*) +- ./references/expo-av-to-audio.md -- Migrate audio playback and recording from expo-av to expo-audio +- ./references/expo-av-to-video.md -- Migrate video playback from expo-av to expo-video + +## Beta/Preview Releases + +Beta versions use `.preview` suffix (e.g., `55.0.0-preview.2`), published under `@next` tag. + +Check if latest is beta: https://exp.host/--/api/v2/versions (look for `-preview` in `expoVersion`) + +```bash +npx expo install expo@next --fix # install beta +``` + +## Step-by-Step Upgrade Process + +1. Upgrade Expo and dependencies + +```bash +npx expo install expo@latest +npx expo install --fix +``` + +2. Run diagnostics: `npx expo-doctor` + +3. Clear caches and reinstall + +```bash +npx expo export -p ios --clear +rm -rf node_modules .expo +watchman watch-del-all +``` + +## Breaking Changes Checklist + +- Check for removed APIs in release notes +- Update import paths for moved modules +- Review native module changes requiring prebuild +- Test all camera, audio, and video features +- Verify navigation still works correctly + +## Prebuild for Native Changes + +**First check if `ios/` and `android/` directories exist in the project.** If neither directory exists, the project uses Continuous Native Generation (CNG) and native projects are regenerated at build time — skip this section and "Clear caches for bare workflow" entirely. + +If upgrading requires native changes: + +```bash +npx expo prebuild --clean +``` + +This regenerates the `ios` and `android` directories. Ensure the project is not a bare workflow app before running this command. + +## Clear caches for bare workflow + +These steps only apply when `ios/` and/or `android/` directories exist in the project: + +- Clear the cocoapods cache for iOS: `cd ios && pod install --repo-update` +- Clear derived data for Xcode: `npx expo run:ios --no-build-cache` +- Clear the Gradle cache for Android: `cd android && ./gradlew clean` + +## Housekeeping + +- Review release notes for the target SDK version at https://expo.dev/changelog +- If using Expo SDK 54 or later, ensure react-native-worklets is installed — this is required for react-native-reanimated to work. +- Enable React Compiler in SDK 54+ by adding `"experiments": { "reactCompiler": true }` to app.json — it's stable and recommended +- Delete sdkVersion from `app.json` to let Expo manage it automatically +- Remove implicit packages from `package.json`: `@babel/core`, `babel-preset-expo`, `expo-constants`. +- If the babel.config.js only contains 'babel-preset-expo', delete the file +- If the metro.config.js only contains expo defaults, delete the file + +## Deprecated Packages + +| Old Package | Replacement | +| -------------------- | ---------------------------------------------------- | +| `expo-av` | `expo-audio` and `expo-video` | +| `expo-permissions` | Individual package permission APIs | +| `@expo/vector-icons` | `expo-symbols` (for SF Symbols) | +| `AsyncStorage` | `expo-sqlite/localStorage/install` | +| `expo-app-loading` | `expo-splash-screen` | +| expo-linear-gradient | experimental_backgroundImage + CSS gradients in View | + +When migrating deprecated packages, update all code usage before removing the old package. For expo-av, consult the migration references to convert Audio.Sound to useAudioPlayer, Audio.Recording to useAudioRecorder, and Video components to VideoView with useVideoPlayer. + +## expo.install.exclude + +Check if package.json has excluded packages: + +```json +{ + "expo": { "install": { "exclude": ["react-native-reanimated"] } } +} +``` + +Exclusions are often workarounds that may no longer be needed after upgrading. Review each one. +## Removing patches + +Check if there are any outdated patches in the `patches/` directory. Remove them if they are no longer needed. + +## Postcss + +- `autoprefixer` isn't needed in SDK +53. Remove it from dependencies and check `postcss.config.js` or `postcss.config.mjs` to remove it from the plugins list. +- Use `postcss.config.mjs` in SDK +53. + +## Metro + +Remove redundant metro config options: + +- resolver.unstable_enablePackageExports is enabled by default in SDK +53. +- `experimentalImportSupport` is enabled by default in SDK +54. +- `EXPO_USE_FAST_RESOLVER=1` is removed in SDK +54. +- cjs and mjs extensions are supported by default in SDK +50. +- Expo webpack is deprecated, migrate to [Expo Router and Metro web](https://docs.expo.dev/router/migrate/from-expo-webpack/). + +## Hermes engine v1 + +Since SDK 55, users can opt-in to use Hermes engine v1 for improved runtime performance. This requires setting `useHermesV1: true` in the `expo-build-properties` config plugin, and may require a specific version of the `hermes-compiler` npm package. Hermes v1 will become a default in some future SDK release. + +## New Architecture + +The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53. diff --git a/.cursor/skills/expo-upgrading-v55/references/expo-av-to-audio.md b/.cursor/skills/expo-upgrading-v55/references/expo-av-to-audio.md new file mode 100644 index 00000000..8a9b53a9 --- /dev/null +++ b/.cursor/skills/expo-upgrading-v55/references/expo-av-to-audio.md @@ -0,0 +1,132 @@ +# Migrating from expo-av to expo-audio + +## Imports + +```tsx +// Before +import { Audio } from 'expo-av'; + +// After +import { useAudioPlayer, useAudioRecorder, RecordingPresets, AudioModule, setAudioModeAsync } from 'expo-audio'; +``` + +## Audio Playback + +### Before (expo-av) + +```tsx +const [sound, setSound] = useState(); + +async function playSound() { + const { sound } = await Audio.Sound.createAsync(require('./audio.mp3')); + setSound(sound); + await sound.playAsync(); +} + +useEffect(() => { + return sound ? () => { sound.unloadAsync(); } : undefined; +}, [sound]); +``` + +### After (expo-audio) + +```tsx +const player = useAudioPlayer(require('./audio.mp3')); + +// Play +player.play(); +``` + +## Audio Recording + +### Before (expo-av) + +```tsx +const [recording, setRecording] = useState(); + +async function startRecording() { + await Audio.requestPermissionsAsync(); + await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true }); + const { recording } = await Audio.Recording.createAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY); + setRecording(recording); +} + +async function stopRecording() { + await recording?.stopAndUnloadAsync(); + const uri = recording?.getURI(); +} +``` + +### After (expo-audio) + +```tsx +const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); + +async function startRecording() { + await AudioModule.requestRecordingPermissionsAsync(); + await recorder.prepareToRecordAsync(); + recorder.record({ forDuration: 60 }); +} + +async function stopRecording() { + await recorder.stop(); + const uri = recorder.uri; +} +``` + +## Audio Mode + +### Before (expo-av) + +```tsx +await Audio.setAudioModeAsync({ + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + staysActiveInBackground: true, + interruptionModeIOS: InterruptionModeIOS.DoNotMix, +}); +``` + +### After (expo-audio) + +```tsx +await setAudioModeAsync({ + playsInSilentMode: true, + shouldPlayInBackground: true, + interruptionMode: 'doNotMix', +}); +``` + +## API Mapping + +| expo-av | expo-audio | +|---------|------------| +| `Audio.Sound.createAsync()` | `useAudioPlayer(source)` | +| `sound.playAsync()` | `player.play()` | +| `sound.pauseAsync()` | `player.pause()` | +| `sound.setPositionAsync(ms)` | `player.seekTo(seconds)` | +| `sound.setVolumeAsync(vol)` | `player.volume = vol` | +| `sound.setRateAsync(rate)` | `player.playbackRate = rate` | +| `sound.setIsLoopingAsync(loop)` | `player.loop = loop` | +| `sound.unloadAsync()` | Automatic via hook | +| `playbackStatus.positionMillis` | `player.currentTime` (seconds) | +| `playbackStatus.durationMillis` | `player.duration` (seconds) | +| `playbackStatus.isPlaying` | `player.playing` | +| `Audio.Recording.createAsync()` | `useAudioRecorder(preset)` | +| `Audio.RecordingOptionsPresets.*` | `RecordingPresets.*` | +| `recording.stopAndUnloadAsync()` | `recorder.stop()` | +| `recording.getURI()` | `recorder.uri` | +| `Audio.requestPermissionsAsync()` | `AudioModule.requestRecordingPermissionsAsync()` | + +## Key Differences + +- **No auto-reset on finish**: After `play()` completes, the player stays paused at the end. To replay, call `player.seekTo(0)` then `play()` +- **Time in seconds**: expo-audio uses seconds, not milliseconds (matching web standards) +- **Immediate loading**: Audio loads immediately when the hook mounts—no explicit preloading needed +- **Automatic cleanup**: No need to call `unloadAsync()`, hooks handle resource cleanup on unmount +- **Multiple players**: Create multiple `useAudioPlayer` instances and store them—all load immediately +- **Direct property access**: Set volume, rate, loop directly on the player object (`player.volume = 0.5`) + +## API Reference + +https://docs.expo.dev/versions/latest/sdk/audio/ diff --git a/.cursor/skills/expo-upgrading-v55/references/expo-av-to-video.md b/.cursor/skills/expo-upgrading-v55/references/expo-av-to-video.md new file mode 100644 index 00000000..5c9bec19 --- /dev/null +++ b/.cursor/skills/expo-upgrading-v55/references/expo-av-to-video.md @@ -0,0 +1,160 @@ +# Migrating from expo-av to expo-video + +## Imports + +```tsx +// Before +import { Video, ResizeMode } from 'expo-av'; + +// After +import { useVideoPlayer, VideoView, VideoSource } from 'expo-video'; +import { useEvent, useEventListener } from 'expo'; +``` + +## Video Playback + +### Before (expo-av) + +```tsx +const videoRef = useRef