diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 4adc6d12..bc4d6997 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -330,6 +330,18 @@ }, "category": "Research" }, + { + "name": "expo", + "source": { + "source": "local", + "path": "./plugins/expo" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" + }, { "name": "coderabbit", "source": { diff --git a/README.md b/README.md index 2e68e165..62a5c6c2 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,5 @@ Highlighted richer examples in this repo include: - `plugins/build-ios-apps` for SwiftUI implementation, refactors, performance, and debugging - `plugins/build-macos-apps` for macOS SwiftUI/AppKit workflows, build/run/debug loops, and packaging guidance - `plugins/build-web-apps` for deployment, UI, payments, and database workflows +- `plugins/expo` for Expo and React Native apps, SDK upgrades, EAS workflows, and Codex Run actions - `plugins/netlify`, `plugins/render`, and `plugins/google-slides` for additional public skill- and MCP-backed plugin bundles diff --git a/plugins/expo/.codex-plugin/plugin.json b/plugins/expo/.codex-plugin/plugin.json new file mode 100644 index 00000000..8a4c3c8b --- /dev/null +++ b/plugins/expo/.codex-plugin/plugin.json @@ -0,0 +1,47 @@ +{ + "name": "expo", + "version": "1.0.0", + "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", + "author": { + "name": "Expo Team", + "email": "support@expo.dev", + "url": "https://expo.dev" + }, + "homepage": "https://docs.expo.dev/skills/", + "repository": "https://github.com/expo/skills/tree/main/plugins/expo", + "license": "MIT", + "keywords": [ + "expo", + "react-native", + "expo-router", + "codex-run-actions", + "eas", + "mobile", + "ios", + "android", + "deployment", + "upgrades", + "native-modules" + ], + "skills": "./skills/", + "interface": { + "displayName": "Expo", + "shortDescription": "Build, deploy, upgrade, and debug Expo and React Native apps", + "longDescription": "Official Expo-authored skills for building Expo Router UI, authoring API routes, configuring data fetching and styling, writing Expo native modules, creating dev clients, upgrading Expo SDKs, wiring Codex app Run actions, and deploying Expo apps with EAS workflows, builds, hosting, TestFlight, App Store, and Play Store guidance.", + "developerName": "Expo", + "category": "Coding", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://expo.dev/", + "privacyPolicyURL": "https://expo.dev/privacy", + "termsOfServiceURL": "https://expo.dev/terms", + "defaultPrompt": "Use Expo when building, deploying, upgrading, debugging, or wiring Codex app Run actions for Expo and React Native apps.", + "composerIcon": "./assets/expo.png", + "logo": "./assets/expo.png", + "screenshots": [], + "brandColor": "#000020" + } +} diff --git a/plugins/expo/LICENSE b/plugins/expo/LICENSE new file mode 100644 index 00000000..548fbcb6 --- /dev/null +++ b/plugins/expo/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025-present 650 Industries, Inc. (aka Expo) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/expo/README.md b/plugins/expo/README.md new file mode 100644 index 00000000..e53ab22e --- /dev/null +++ b/plugins/expo/README.md @@ -0,0 +1,84 @@ +# Expo + +Official AI agent skills from the Expo team for building, deploying, upgrading, and debugging Expo apps. + +## What This Plugin Does + +### App Design + +- Provides UI guidelines following Apple Human Interface Guidelines +- Covers Expo Router navigation patterns (stacks, tabs, modals, sheets) +- Explains native iOS controls, SF Symbols, animations, and visual effects +- Guides API route creation with EAS Hosting +- Covers data fetching patterns with React Query, offline support, and Expo Router loaders +- Helps set up Tailwind CSS v4 with NativeWind v5 +- Explains DOM components for running web code in native apps +- Wires Expo projects into the Codex app Run button and action terminal + +### Deployment + +- Guides iOS App Store, TestFlight, and Android Play Store submissions +- Covers EAS Build configuration and version management +- Helps write and validate EAS Workflow YAML files for CI/CD +- Covers web deployment with EAS Hosting + +### Upgrading + +- Walks through the step-by-step Expo SDK upgrade process +- Identifies deprecated packages and their modern replacements +- Handles cache clearing for both managed and bare workflows +- Fixes dependency conflicts after an upgrade + +## When to Use + +### App Design + +- Building new Expo apps from scratch +- Adding navigation, styling, or animations +- Setting up API routes or data fetching +- Integrating web libraries via DOM components +- Configuring Tailwind CSS for React Native +- Adding a Codex app Run button for `expo start` +- Creating optional Codex action buttons for iOS, Android, Web, dev-client, diagnostics, or export + +### Deployment + +- Submitting apps to App Store Connect or Google Play +- Setting up TestFlight beta testing +- Configuring EAS Build profiles +- Writing CI/CD workflows for automated deployments +- Deploying web apps with EAS Hosting + +### Upgrading + +- Upgrading to a new Expo SDK version +- Fixing dependency conflicts after an upgrade +- Migrating from deprecated packages (expo-av to expo-audio/expo-video) +- Cleaning up legacy configuration files + +## Skills Included + +### App Design + +- **building-native-ui** — Build beautiful apps with Expo Router, styling, components, navigation, and animations +- **codex-expo-run-actions** — Wire `script/build_and_run.sh` and `.codex/environments/environment.toml` so the Codex app Run button starts Expo +- **expo-api-routes** — Create API routes in Expo Router with EAS Hosting +- **expo-dev-client** — Build and distribute Expo development clients locally or via TestFlight +- **expo-tailwind-setup** — Set up Tailwind CSS v4 in Expo with NativeWind v5 +- **expo-ui-jetpack-compose** — Jetpack Compose UI components for Expo +- **expo-ui-swift-ui** — SwiftUI components for Expo +- **native-data-fetching** — Network requests, API calls, caching, and offline support +- **use-dom** — Run web code in a webview on native using DOM components + +### Deployment + +- **expo-deployment** — Deploy to iOS App Store, Android Play Store, and web hosting +- **expo-cicd-workflows** — EAS workflow YAML files for CI/CD pipelines + +### Upgrading + +- **upgrading-expo** — Upgrade Expo SDK versions and fix dependency issues + +## License + +MIT diff --git a/plugins/expo/agents/openai.yaml b/plugins/expo/agents/openai.yaml new file mode 100644 index 00000000..fc7c5ff4 --- /dev/null +++ b/plugins/expo/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Expo" + short_description: "Build, deploy, upgrade, and debug Expo and React Native apps" + icon_small: "./assets/expo.png" + icon_large: "./assets/expo.png" + default_prompt: "Use Expo when building Expo Router UI, API routes, native modules, dev clients, data fetching, styling, Codex app Run actions, EAS workflows, deployments, or SDK upgrades." diff --git a/plugins/expo/assets/expo.png b/plugins/expo/assets/expo.png new file mode 100644 index 00000000..e2a35b12 Binary files /dev/null and b/plugins/expo/assets/expo.png differ diff --git a/plugins/expo/commands/setup-codex-run-actions.md b/plugins/expo/commands/setup-codex-run-actions.md new file mode 100644 index 00000000..86008b2d --- /dev/null +++ b/plugins/expo/commands/setup-codex-run-actions.md @@ -0,0 +1,24 @@ +# /setup-codex-run-actions + +Wire the current Expo project into the Codex app action bar. + +## Arguments + +- `root`: Expo app root, if not the current directory (optional) +- `actions`: comma-separated extra buttons such as `ios,android,web,dev-client,doctor,export-web` (optional) +- `run`: whether to start the Expo dev server after setup (optional, default: false) + +## Workflow + +1. Use the `codex-expo-run-actions` skill. +2. Confirm the target root is an Expo app before editing. +3. Create or update `script/build_and_run.sh` using the skill reference. +4. Create or update `.codex/environments/environment.toml`. +5. Always wire one primary `Run` action to `./script/build_and_run.sh`. +6. Add optional actions only when requested. +7. Validate with `bash -n ./script/build_and_run.sh` and `./script/build_and_run.sh --help`. +8. Start the dev server only when `run=true` or the user explicitly asks. + +## Output + +Report the script path, environment file path, action names, and validation command results. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md new file mode 100644 index 00000000..840518fc --- /dev/null +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -0,0 +1,321 @@ +--- +name: building-native-ui +description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs. +version: 1.0.1 +license: MIT +--- + +# Expo UI Guidelines + +## References + +Consult these resources as needed: + +``` +references/ + animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures + controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker + form-sheet.md Form sheets in expo-router: configuration, footers and background interaction. + gradients.md CSS gradients via experimental_backgroundImage (New Arch only) + icons.md SF Symbols via expo-image (sf: source), names, animations, weights + media.md Camera, audio, video, and file saving + route-structure.md Route conventions, dynamic routes, groups, folder organization + search.md Search bar with headers, useSearch hook, filtering patterns + storage.md SQLite, AsyncStorage, SecureStore + tabs.md NativeTabs, migration from JS tabs, iOS 26 features + toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only) + visual-effects.md Blur (expo-blur) and liquid glass (expo-glass-effect) + webgpu-three.md 3D graphics, games, GPU visualizations with WebGPU and Three.js + zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+) +``` + +## Running the App + +**CRITICAL: Always try Expo Go first before creating custom builds.** + +Most Expo apps work in Expo Go without any custom native code. Before running `npx expo run:ios` or `npx expo run:android`: + +1. **Start with Expo Go**: Run `npx expo start` and scan the QR code with Expo Go +2. **Check if features work**: Test your app thoroughly in Expo Go +3. **Only create custom builds when required** - see below + +### When Custom Builds Are Required + +You need `npx expo run:ios/android` or `eas build` ONLY when using: + +- **Local Expo modules** (custom native code in `modules/`) +- **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`) +- **Third-party native modules** not included in Expo Go +- **Custom native configuration** that can't be expressed in `app.json` + +### When Expo Go Works + +Expo Go supports a huge range of features out of the box: + +- All `expo-*` packages (camera, location, notifications, etc.) +- Expo Router navigation +- Most UI libraries (reanimated, gesture handler, etc.) +- Push notifications, deep links, and more + +**If you're unsure, try Expo Go first.** Creating custom builds adds complexity, slower iteration, and requires Xcode/Android Studio setup. + +## Code Style + +- Be cautious of unterminated strings. Ensure nested backticks are escaped; never forget to escape quotes correctly. +- Always use import statements at the top of the file. +- Always use kebab-case for file names, e.g. `comment-card.tsx` +- Always remove old route files when moving or restructuring navigation +- Never use special characters in file names +- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors. + +## Routes + +See `./references/route-structure.md` for detailed route conventions. + +- Routes belong in the `app` directory. +- Never co-locate components, types, or utilities in the app directory. This is an anti-pattern. +- Ensure the app always has a route that matches "/", it may be inside a group route. + +## Library Preferences + +- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage +- Never use legacy expo-permissions +- `expo-audio` not `expo-av` +- `expo-video` not `expo-av` +- `expo-image` with `source="sf:name"` for SF Symbols, not `expo-symbols` or `@expo/vector-icons` +- `react-native-safe-area-context` not react-native SafeAreaView +- `process.env.EXPO_OS` not `Platform.OS` +- `React.use` not `React.useContext` +- `expo-image` Image component instead of intrinsic element `img` +- `expo-glass-effect` for liquid glass backdrops + +## Responsiveness + +- Always wrap root component in a scroll view for responsiveness +- Use `` instead of `` for smarter safe area insets +- `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList as well +- Use flexbox instead of Dimensions API +- ALWAYS prefer `useWindowDimensions` over `Dimensions.get()` to measure screen size + +## Behavior + +- Use expo-haptics conditionally on iOS to make more delightful experiences +- Use views with built-in haptics like `` from React Native and `@react-native-community/datetimepicker` +- When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set +- When adding a `ScrollView` to the page it should almost always be the first component inside the route component +- Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar +- Use the `` prop on text containing data that could be copied +- Consider formatting large numbers like 1.4M or 38k +- Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component + +# Styling + +Follow Apple Human Interface Guidelines. + +## General Styling Rules + +- Prefer flex gap over margin and padding styles +- Prefer padding over margin where possible +- Always account for safe area, either with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"` +- Ensure both top and bottom safe area insets are accounted for +- Inline styles not StyleSheet.create unless reusing styles is faster +- Add entering and exiting animations for state changes +- Use `{ borderCurve: 'continuous' }` for rounded corners unless creating a capsule shape +- ALWAYS use a navigation stack title instead of a custom text element on the page +- When padding a ScrollView, use `contentContainerStyle` padding and gap instead of padding on the ScrollView itself (reduces clipping) +- CSS and Tailwind are not supported - use inline styles + +## Text Styling + +- Add the `selectable` prop to every `` element displaying important data or error messages +- Counters should use `{ fontVariant: 'tabular-nums' }` for alignment + +## Shadows + +Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles. + +```tsx + +``` + +'inset' shadows are supported. + +# Navigation + +## Link + +Use `` from 'expo-router' for navigation between routes. + +```tsx +import { Link } from 'expo-router'; + +// Basic link + + +// Wrapping custom components + + ... + +``` + +Whenever possible, include a `` to follow iOS conventions. Add context menus and previews frequently to enhance navigation. + +## Stack + +- ALWAYS use `_layout.tsx` files to define stacks +- Use Stack from 'expo-router/stack' for native navigation stacks + +### Page Title + +Set the page title in Stack.Screen options: + +```tsx + +``` + +## Context Menus + +Add long press context menus to Link components: + +```tsx +import { Link } from "expo-router"; + + + + + + + + + + + + {}} /> + {}} + /> + + +; +``` + +## Link Previews + +Use link previews frequently to enhance navigation: + +```tsx + + + + + + + + +``` + +Link preview can be used with context menus. + +## Modal + +Present a screen as a modal: + +```tsx + +``` + +Prefer this to building a custom modal component. + +## Sheet + +Present a screen as a dynamic form sheet: + +```tsx + +``` + +- Using `contentStyle: { backgroundColor: "transparent" }` makes the background liquid glass on iOS 26+. + +## Common route structure + +A standard app layout with tabs and stacks inside each tab: + +``` +app/ + _layout.tsx — + (index,search)/ + _layout.tsx — + index.tsx — Main list + search.tsx — Search view +``` + +```tsx +// app/_layout.tsx +import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs"; +import { Theme } from "../components/theme"; + +export default function Layout() { + return ( + + + + + + + + + + ); +} +``` + +Create a shared group route so both tabs can push common screens: + +```tsx +// app/(index,search)/_layout.tsx +import { Stack } from "expo-router/stack"; +import { PlatformColor } from "react-native"; + +export default function Layout({ segment }) { + const screen = segment.match(/\((.*)\)/)?.[1]!; + const titles: Record = { index: "Items", search: "Search" }; + + return ( + + + + + ); +} +``` diff --git a/plugins/expo/skills/building-native-ui/agents/openai.yaml b/plugins/expo/skills/building-native-ui/agents/openai.yaml new file mode 100644 index 00000000..d5150503 --- /dev/null +++ b/plugins/expo/skills/building-native-ui/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Building Native UI" + short_description: "Build beautiful Expo Router UI with native-feeling navigation, controls, media, animation, and visual effects" + default_prompt: "Use $building-native-ui to build or refactor Expo Router UI, choose native-feeling navigation and controls, structure routes, style screens, and decide when Expo Go is enough before creating native builds." diff --git a/plugins/expo/skills/building-native-ui/references/animations.md b/plugins/expo/skills/building-native-ui/references/animations.md new file mode 100644 index 00000000..657cad8a --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/animations.md @@ -0,0 +1,220 @@ +# Animations + +Use Reanimated v4. Avoid React Native's built-in Animated API. + +## Entering and Exiting Animations + +Use Animated.View with entering and exiting animations. Layout animations can animate state changes. + +```tsx +import Animated, { + FadeIn, + FadeOut, + LinearTransition, +} from "react-native-reanimated"; + +function App() { + return ( + + ); +} +``` + +## On-Scroll Animations + +Create high-performance scroll animations using Reanimated's hooks: + +```tsx +import Animated, { + useAnimatedRef, + useScrollViewOffset, + useAnimatedStyle, + interpolate, +} from "react-native-reanimated"; + +function Page() { + const ref = useAnimatedRef(); + const scroll = useScrollViewOffset(ref); + + const style = useAnimatedStyle(() => ({ + opacity: interpolate(scroll.value, [0, 30], [0, 1], "clamp"), + })); + + return ( + + + + ); +} +``` + +## Common Animation Presets + +### Entering Animations + +- `FadeIn`, `FadeInUp`, `FadeInDown`, `FadeInLeft`, `FadeInRight` +- `SlideInUp`, `SlideInDown`, `SlideInLeft`, `SlideInRight` +- `ZoomIn`, `ZoomInUp`, `ZoomInDown` +- `BounceIn`, `BounceInUp`, `BounceInDown` + +### Exiting Animations + +- `FadeOut`, `FadeOutUp`, `FadeOutDown`, `FadeOutLeft`, `FadeOutRight` +- `SlideOutUp`, `SlideOutDown`, `SlideOutLeft`, `SlideOutRight` +- `ZoomOut`, `ZoomOutUp`, `ZoomOutDown` +- `BounceOut`, `BounceOutUp`, `BounceOutDown` + +### Layout Animations + +- `LinearTransition` — Smooth linear interpolation +- `SequencedTransition` — Sequenced property changes +- `FadingTransition` — Fade between states + +## Customizing Animations + +```tsx + +``` + +### Modifiers + +```tsx +// Duration in milliseconds +FadeIn.duration(300); + +// Delay before starting +FadeIn.delay(100); + +// Spring physics +FadeIn.springify(); +FadeIn.springify().damping(15).stiffness(100); + +// Easing curves +FadeIn.easing(Easing.bezier(0.25, 0.1, 0.25, 1)); + +// Chaining +FadeInDown.duration(400).delay(200).springify(); +``` + +## Shared Value Animations + +For imperative control over animations: + +```tsx +import { + useSharedValue, + withSpring, + withTiming, +} from "react-native-reanimated"; + +const offset = useSharedValue(0); + +// Spring animation +offset.value = withSpring(100); + +// Timing animation +offset.value = withTiming(100, { duration: 300 }); + +// Use in styles +const style = useAnimatedStyle(() => ({ + transform: [{ translateX: offset.value }], +})); +``` + +## Gesture Animations + +Combine with React Native Gesture Handler: + +```tsx +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, +} from "react-native-reanimated"; + +function DraggableBox() { + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + + const gesture = Gesture.Pan() + .onUpdate((e) => { + translateX.value = e.translationX; + translateY.value = e.translationY; + }) + .onEnd(() => { + translateX.value = withSpring(0); + translateY.value = withSpring(0); + }); + + const style = useAnimatedStyle(() => ({ + transform: [ + { translateX: translateX.value }, + { translateY: translateY.value }, + ], + })); + + return ( + + + + ); +} +``` + +## Keyboard Animations + +Animate with keyboard height changes: + +```tsx +import Animated, { + useAnimatedKeyboard, + useAnimatedStyle, +} from "react-native-reanimated"; + +function KeyboardAwareView() { + const keyboard = useAnimatedKeyboard(); + + const style = useAnimatedStyle(() => ({ + paddingBottom: keyboard.height.value, + })); + + return {/* content */}; +} +``` + +## Staggered List Animations + +Animate list items with delays: + +```tsx +{ + items.map((item, index) => ( + + + + )); +} +``` + +## Best Practices + +- Add entering and exiting animations for state changes +- Use layout animations when items are added/removed from lists +- Use `useAnimatedStyle` for scroll-driven animations +- Prefer `interpolate` with "clamp" for bounded values +- You can't pass PlatformColors to reanimated views or styles; use static colors instead +- Keep animations under 300ms for responsive feel +- Use spring animations for natural movement +- Avoid animating layout properties (width, height) when possible — prefer transforms diff --git a/plugins/expo/skills/building-native-ui/references/controls.md b/plugins/expo/skills/building-native-ui/references/controls.md new file mode 100644 index 00000000..762fe208 --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/controls.md @@ -0,0 +1,270 @@ +# Native Controls + +Native iOS controls provide built-in haptics, accessibility, and platform-appropriate styling. + +## Switch + +Use for binary on/off settings. Has built-in haptics. + +```tsx +import { Switch } from "react-native"; +import { useState } from "react"; + +const [enabled, setEnabled] = useState(false); + +; +``` + +### Customization + +```tsx + +``` + +## Segmented Control + +Use for non-navigational tabs or mode selection. Avoid changing default colors. + +```tsx +import SegmentedControl from "@react-native-segmented-control/segmented-control"; +import { useState } from "react"; + +const [index, setIndex] = useState(0); + + setIndex(nativeEvent.selectedSegmentIndex)} +/>; +``` + +### Rules + +- Maximum 4 options — use a picker for more +- Keep labels short (1-2 words) +- Avoid custom colors — native styling adapts to dark mode + +### With Icons (iOS 14+) + +```tsx + setIndex(nativeEvent.selectedSegmentIndex)} +/> +``` + +## Slider + +Continuous value selection. + +```tsx +import Slider from "@react-native-community/slider"; +import { useState } from "react"; + +const [value, setValue] = useState(0.5); + +; +``` + +### Customization + +```tsx + +``` + +### Discrete Steps + +```tsx + +``` + +## Date/Time Picker + +Compact pickers with popovers. Has built-in haptics. + +```tsx +import DateTimePicker from "@react-native-community/datetimepicker"; +import { useState } from "react"; + +const [date, setDate] = useState(new Date()); + + { + if (selectedDate) setDate(selectedDate); + }} + mode="datetime" +/>; +``` + +### Modes + +- `date` — Date only +- `time` — Time only +- `datetime` — Date and time + +### Display Styles + +```tsx +// Compact inline (default) + + +// Spinner wheel + + +// Full calendar + +``` + +### Time Intervals + +```tsx + +``` + +### Min/Max Dates + +```tsx + +``` + +## Stepper + +Increment/decrement numeric values. + +```tsx +import { Stepper } from "react-native"; +import { useState } from "react"; + +const [count, setCount] = useState(0); + +; +``` + +## TextInput + +Native text input with various keyboard types. + +```tsx +import { TextInput } from "react-native"; + + +``` + +### Keyboard Types + +```tsx +// Email + + +// Phone + + +// Number + + +// Password + + +// Search + +``` + +### Multiline + +```tsx + +``` + +## Picker (Wheel) + +For selection from many options (5+ items). + +```tsx +import { Picker } from "@react-native-picker/picker"; +import { useState } from "react"; + +const [selected, setSelected] = useState("js"); + + + + + + +; +``` + +## Best Practices + +- **Haptics**: Switch and DateTimePicker have built-in haptics — don't add extra +- **Accessibility**: Native controls have proper accessibility labels by default +- **Dark Mode**: Avoid custom colors — native styling adapts automatically +- **Spacing**: Use consistent padding around controls (12-16pt) +- **Labels**: Place labels above or to the left of controls +- **Grouping**: Group related controls in sections with headers diff --git a/plugins/expo/skills/building-native-ui/references/form-sheet.md b/plugins/expo/skills/building-native-ui/references/form-sheet.md new file mode 100644 index 00000000..1ed80fb6 --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/form-sheet.md @@ -0,0 +1,253 @@ +# Form Sheets in Expo Router + +This skill covers implementing form sheets with footers using Expo Router's Stack navigator and react-native-screens. + +## Overview + +Form sheets are modal presentations that appear as a card sliding up from the bottom of the screen. They're ideal for: + +- Quick actions and confirmations +- Settings panels +- Login/signup flows +- Action sheets with custom content + +**Requirements:** + +- Expo Router Stack navigator + +## Basic Usage + +### Form Sheet with Footer + +Configure the Stack.Screen with transparent backgrounds and sheet presentation: + +```tsx +// app/_layout.tsx +import { Stack } from "expo-router"; + +export default function Layout() { + return ( + + + + + + + ); +} +``` + +### Form Sheet Screen Content + +> Requires Expo SDK 55 or later. + +Use `flex: 1` to allow the content to fill available space, enabling footer positioning: + +```tsx +// app/about.tsx +import { View, Text, StyleSheet } from "react-native"; + +export default function AboutSheet() { + return ( + + {/* Main content */} + + Sheet Content + + + {/* Footer - stays at bottom */} + + Footer Content + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + content: { + flex: 1, + padding: 16, + }, + footer: { + padding: 16, + }, +}); +``` + +### Formsheet with interactive content below + +Use `sheetLargestUndimmedDetentIndex` (zero-indexed) to keep content behind the form sheet interactive — e.g. letting users pan a map beneath it. Setting it to `1` allows interaction at the first two detents but dims on the third. + +```tsx +// app/_layout.tsx +import { Stack } from 'expo-router'; + +export default function Layout() { + return ( + + + + + ) +} +``` + +## Key Options + +| Option | Type | Description | +| --------------------- | ---------- | ----------------------------------------------------------- | +| `presentation` | `string` | Set to `'formSheet'` for sheet presentation | +| `sheetGrabberVisible` | `boolean` | Shows the drag handle at the top of the sheet | +| `sheetAllowedDetents` | `number[]` | Array of detent heights (0-1 range, e.g., `[0.25]` for 25%) | +| `headerTransparent` | `boolean` | Makes header background transparent | +| `contentStyle` | `object` | Style object for the screen content container | +| `title` | `string` | Screen title (set to `''` for no title) | + +## Common Detent Values + +- `[0.25]` - Quarter sheet (compact actions) +- `[0.5]` - Half sheet (medium content) +- `[0.75]` - Three-quarter sheet (detailed forms) +- `[0.25, 0.5, 1]` - Multiple stops (expandable sheet) + +## Complete Example + +```tsx +// _layout.tsx +import { Stack } from "expo-router"; + +export default function Layout() { + return ( + + + + + + + + + ); +} +``` + +```tsx +// app/confirm.tsx +import { View, Text, Pressable, StyleSheet } from "react-native"; +import { router } from "expo-router"; + +export default function ConfirmSheet() { + return ( + + + Confirm Action + + Are you sure you want to proceed? + + + + + router.back()}> + Cancel + + router.back()}> + Confirm + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + content: { + flex: 1, + padding: 20, + alignItems: "center", + justifyContent: "center", + }, + title: { + fontSize: 18, + fontWeight: "600", + marginBottom: 8, + }, + description: { + fontSize: 14, + color: "#666", + textAlign: "center", + }, + footer: { + flexDirection: "row", + padding: 16, + gap: 12, + }, + cancelButton: { + flex: 1, + padding: 14, + borderRadius: 10, + backgroundColor: "#f0f0f0", + alignItems: "center", + }, + cancelText: { + fontSize: 16, + fontWeight: "500", + }, + confirmButton: { + flex: 1, + padding: 14, + borderRadius: 10, + backgroundColor: "#007AFF", + alignItems: "center", + }, + confirmText: { + fontSize: 16, + fontWeight: "500", + color: "white", + }, +}); +``` + +## Troubleshooting + +### Content not filling sheet + +Make sure the root View uses `flex: 1`: + +```tsx +{/* content */} +``` + +### Sheet background showing through + +Set `contentStyle: { backgroundColor: 'transparent' }` in options and style your content container with the desired background color instead. diff --git a/plugins/expo/skills/building-native-ui/references/gradients.md b/plugins/expo/skills/building-native-ui/references/gradients.md new file mode 100644 index 00000000..329600dc --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/gradients.md @@ -0,0 +1,106 @@ +# CSS Gradients + +> **New Architecture Only**: CSS gradients require React Native's New Architecture (Fabric). They are not available in the old architecture or Expo Go. + +Use CSS gradients with the `experimental_backgroundImage` style property. + +## Linear Gradients + +```tsx +// Top to bottom + + +// Left to right + + +// Diagonal + + +// Using degrees + +``` + +## Radial Gradients + +```tsx +// Circle at center + + +// Ellipse + + +// Positioned + +``` + +## Multiple Gradients + +Stack multiple gradients by comma-separating them: + +```tsx + +``` + +## Common Patterns + +### Overlay on Image + +```tsx + + + + +``` + +### Frosted Glass Effect + +```tsx + +``` + +### Button Gradient + +```tsx + + Submit + +``` + +## Important Notes + +- Do NOT use `expo-linear-gradient` — use CSS gradients instead +- Gradients are strings, not objects +- Use `rgba()` for transparency, or `transparent` keyword +- Color stops use percentages (0%, 50%, 100%) +- Direction keywords: `to top`, `to bottom`, `to left`, `to right`, `to top left`, etc. +- Degree values: `45deg`, `90deg`, `135deg`, etc. diff --git a/plugins/expo/skills/building-native-ui/references/icons.md b/plugins/expo/skills/building-native-ui/references/icons.md new file mode 100644 index 00000000..eebf674f --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/icons.md @@ -0,0 +1,213 @@ +# Icons (SF Symbols) + +Use SF Symbols for native feel. Never use FontAwesome or Ionicons. + +## Basic Usage + +```tsx +import { SymbolView } from "expo-symbols"; +import { PlatformColor } from "react-native"; + +; +``` + +## Props + +```tsx + +``` + +## Common Icons + +### Navigation & Actions +- `house.fill` - home +- `gear` - settings +- `magnifyingglass` - search +- `plus` - add +- `xmark` - close +- `chevron.left` - back +- `chevron.right` - forward +- `arrow.left` - back arrow +- `arrow.right` - forward arrow + +### Media +- `play.fill` - play +- `pause.fill` - pause +- `stop.fill` - stop +- `backward.fill` - rewind +- `forward.fill` - fast forward +- `speaker.wave.2.fill` - volume +- `speaker.slash.fill` - mute + +### Camera +- `camera` - camera +- `camera.fill` - camera filled +- `arrow.triangle.2.circlepath` - flip camera +- `photo` - gallery/photos +- `bolt` - flash +- `bolt.slash` - flash off + +### Communication +- `message` - message +- `message.fill` - message filled +- `envelope` - email +- `envelope.fill` - email filled +- `phone` - phone +- `phone.fill` - phone filled +- `video` - video call +- `video.fill` - video call filled + +### Social +- `heart` - like +- `heart.fill` - liked +- `star` - favorite +- `star.fill` - favorited +- `hand.thumbsup` - thumbs up +- `hand.thumbsdown` - thumbs down +- `person` - profile +- `person.fill` - profile filled +- `person.2` - people +- `person.2.fill` - people filled + +### Content Actions +- `square.and.arrow.up` - share +- `square.and.arrow.down` - download +- `doc.on.doc` - copy +- `trash` - delete +- `pencil` - edit +- `folder` - folder +- `folder.fill` - folder filled +- `bookmark` - bookmark +- `bookmark.fill` - bookmarked + +### Status & Feedback +- `checkmark` - success/done +- `checkmark.circle.fill` - completed +- `xmark.circle.fill` - error/failed +- `exclamationmark.triangle` - warning +- `info.circle` - info +- `questionmark.circle` - help +- `bell` - notification +- `bell.fill` - notification filled + +### Misc +- `ellipsis` - more options +- `ellipsis.circle` - more in circle +- `line.3.horizontal` - menu/hamburger +- `slider.horizontal.3` - filters +- `arrow.clockwise` - refresh +- `location` - location +- `location.fill` - location filled +- `map` - map +- `mappin` - pin +- `clock` - time +- `calendar` - calendar +- `link` - link +- `nosign` - block/prohibited + +## Animated Symbols + +```tsx + +``` + +### Animation Effects + +- `bounce` - Bouncy animation +- `pulse` - Pulsing effect +- `variableColor` - Color cycling +- `scale` - Scale animation + +```tsx +// Bounce with direction +animationSpec={{ + effect: { type: "bounce", direction: "up" } // up | down +}} + +// Pulse +animationSpec={{ + effect: { type: "pulse" } +}} + +// Variable color (multicolor symbols) +animationSpec={{ + effect: { + type: "variableColor", + cumulative: true, + reversing: true + } +}} +``` + +## Symbol Weights + +```tsx +// Lighter weights + + + + +// Default + + +// Heavier weights + + + + + +``` + +## Symbol Scales + +```tsx + + // default + +``` + +## Multicolor Symbols + +Some symbols support multiple colors: + +```tsx + +``` + +## Finding Symbol Names + +1. Use the SF Symbols app on macOS (free from Apple) +2. Search at https://developer.apple.com/sf-symbols/ +3. Symbol names use dot notation: `square.and.arrow.up` + +## Best Practices + +- Always use SF Symbols over vector icon libraries +- Match symbol weight to nearby text weight +- Use `.fill` variants for selected/active states +- Use PlatformColor for tint to support dark mode +- Keep icons at consistent sizes (16, 20, 24, 32) diff --git a/plugins/expo/skills/building-native-ui/references/media.md b/plugins/expo/skills/building-native-ui/references/media.md new file mode 100644 index 00000000..50c0ffb9 --- /dev/null +++ b/plugins/expo/skills/building-native-ui/references/media.md @@ -0,0 +1,198 @@ +# Media + +## Camera + +- Hide navigation headers when there's a full screen camera +- Ensure to flip the camera with `mirror` to emulate social apps +- Use liquid glass buttons on cameras +- Icons: `arrow.triangle.2.circlepath` (flip), `photo` (gallery), `bolt` (flash) +- Eagerly request camera permission +- Lazily request media library permission + +```tsx +import React, { useRef, useState } from "react"; +import { View, TouchableOpacity, Text, Alert } from "react-native"; +import { CameraView, CameraType, useCameraPermissions } from "expo-camera"; +import * as MediaLibrary from "expo-media-library"; +import * as ImagePicker from "expo-image-picker"; +import * as Haptics from "expo-haptics"; +import { SymbolView } from "expo-symbols"; +import { PlatformColor } from "react-native"; +import { GlassView } from "expo-glass-effect"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +function Camera({ onPicture }: { onPicture: (uri: string) => Promise }) { + const [permission, requestPermission] = useCameraPermissions(); + const cameraRef = useRef(null); + const [type, setType] = useState("back"); + const { bottom } = useSafeAreaInsets(); + + if (!permission?.granted) { + return ( + + Camera access is required + + + Grant Permission + + + + ); + } + + const takePhoto = async () => { + await Haptics.selectionAsync(); + if (!cameraRef.current) return; + const photo = await cameraRef.current.takePictureAsync({ quality: 0.8 }); + await onPicture(photo.uri); + }; + + const selectPhoto = async () => { + await Haptics.selectionAsync(); + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: "images", + allowsEditing: false, + quality: 0.8, + }); + if (!result.canceled && result.assets?.[0]) { + await onPicture(result.assets[0].uri); + } + }; + + return ( + + + + + + + + + setType(t => t === "back" ? "front" : "back")} icon="arrow.triangle.2.circlepath" /> + + + + ); +} +``` + +## Audio Playback + +Use `expo-audio` not `expo-av`: + +```tsx +import { useAudioPlayer } from 'expo-audio'; + +const player = useAudioPlayer({ uri: 'https://stream.nightride.fm/rektory.mp3' }); + + + +; +``` + +## Key Components + +- **LazyColumn** — Use instead of react-native `ScrollView`/`FlatList` for scrollable lists. Wrap in ``. +- **Icon** — Use `` with Android XML vector drawables. To get icons: go to [Material Symbols](https://fonts.google.com/icons), select an icon, choose the Android platform, and download the XML vector drawable. Save these as `.xml` files in your project's `assets/` directory (e.g. `assets/icons/wifi.xml`). Metro bundles `.xml` assets automatically — no metro config changes needed. diff --git a/plugins/expo/skills/expo-ui-jetpack-compose/agents/openai.yaml b/plugins/expo/skills/expo-ui-jetpack-compose/agents/openai.yaml new file mode 100644 index 00000000..98580382 --- /dev/null +++ b/plugins/expo/skills/expo-ui-jetpack-compose/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Expo UI Jetpack Compose" + short_description: "Use @expo/ui/jetpack-compose views and modifiers inside Expo React Native apps" + default_prompt: "Use $expo-ui-jetpack-compose when adding or reviewing @expo/ui/jetpack-compose Host trees, Compose-like components, modifiers, Material-style UI, and Android vector icon assets." diff --git a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md new file mode 100644 index 00000000..b5ca16e1 --- /dev/null +++ b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md @@ -0,0 +1,39 @@ +--- +name: expo-ui-swift-ui +description: "`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app." +--- + +> The instructions in this skill apply to SDK 55 only. For other SDK versions, refer to the Expo UI SwiftUI docs for that version for the most accurate information. + +## Installation + +```bash +npx expo install @expo/ui +``` + +A native rebuild is required after installation (`npx expo run:ios`). + +## Instructions + +- Expo UI's API mirrors SwiftUI's API. Use SwiftUI knowledge to decide which components or modifiers to use. +- Components are imported from `@expo/ui/swift-ui`, modifiers from `@expo/ui/swift-ui/modifiers`. +- When about to use a component, fetch its docs to confirm the API - https://docs.expo.dev/versions/v55.0.0/sdk/ui/swift-ui/{component-name}/index.md +- When unsure about a modifier's API, refer to the docs - https://docs.expo.dev/versions/v55.0.0/sdk/ui/swift-ui/modifiers/index.md +- Every SwiftUI tree must be wrapped in `Host`. +- `RNHostView` is specifically for embedding RN components inside a SwiftUI tree. Example: + +```jsx +import { Host, VStack, RNHostView } from "@expo-ui/swift-ui"; +import { Pressable } from "react-native"; + + + + + // Here, `Pressable` is an RN component so it is wrapped in `RNHostView`. + + + +; +``` + +- If a required modifier or View is missing in Expo UI, it can be extended via a local Expo module. See: https://docs.expo.dev/guides/expo-ui-swift-ui/extending/index.md. Confirm with the user before extending. diff --git a/plugins/expo/skills/expo-ui-swift-ui/agents/openai.yaml b/plugins/expo/skills/expo-ui-swift-ui/agents/openai.yaml new file mode 100644 index 00000000..8b1e672f --- /dev/null +++ b/plugins/expo/skills/expo-ui-swift-ui/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Expo UI SwiftUI" + short_description: "Use @expo/ui/swift-ui views and modifiers inside Expo React Native apps" + default_prompt: "Use $expo-ui-swift-ui when adding or reviewing @expo/ui/swift-ui Host trees, SwiftUI-like components, modifiers, RNHostView usage, and local Expo-module extensions for missing SwiftUI capabilities." diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md new file mode 100644 index 00000000..d59cb38e --- /dev/null +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -0,0 +1,507 @@ +--- +name: native-data-fetching +description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`). +version: 1.0.0 +license: MIT +--- + +# Expo Networking + +**You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.** + +## References + +Consult these resources as needed: + +``` +references/ + expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+) +``` + +## When to Use + +Use this skill when: + +- Implementing API requests +- Setting up data fetching (React Query, SWR) +- Using Expo Router data loaders (`useLoaderData`, web SDK 55+) +- Debugging network failures +- Implementing caching strategies +- Handling offline scenarios +- Authentication/token management +- Configuring API URLs and environment variables + +## Preferences + +- Avoid axios, prefer expo/fetch + +## Common Issues & Solutions + +### 1. Basic Fetch Usage + +**Simple GET request**: + +```tsx +const fetchUser = async (userId: string) => { + const response = await fetch(`https://api.example.com/users/${userId}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +}; +``` + +**POST request with body**: + +```tsx +const createUser = async (userData: UserData) => { + const response = await fetch("https://api.example.com/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message); + } + + return response.json(); +}; +``` + +--- + +### 2. React Query (TanStack Query) + +**Setup**: + +```tsx +// app/_layout.tsx +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 2, + }, + }, +}); + +export default function RootLayout() { + return ( + + + + ); +} +``` + +**Fetching data**: + +```tsx +import { useQuery } from "@tanstack/react-query"; + +function UserProfile({ userId }: { userId: string }) { + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["user", userId], + queryFn: () => fetchUser(userId), + }); + + if (isLoading) return ; + if (error) return ; + + return ; +} +``` + +**Mutations**: + +```tsx +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +function CreateUserForm() { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: createUser, + onSuccess: () => { + // Invalidate and refetch + queryClient.invalidateQueries({ queryKey: ["users"] }); + }, + }); + + const handleSubmit = (data: UserData) => { + mutation.mutate(data); + }; + + return
; +} +``` + +--- + +### 3. Error Handling + +**Comprehensive error handling**: + +```tsx +class ApiError extends Error { + constructor(message: string, public status: number, public code?: string) { + super(message); + this.name = "ApiError"; + } +} + +const fetchWithErrorHandling = async (url: string, options?: RequestInit) => { + try { + const response = await fetch(url, options); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new ApiError( + error.message || "Request failed", + response.status, + error.code + ); + } + + return response.json(); + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + // Network error (no internet, timeout, etc.) + throw new ApiError("Network error", 0, "NETWORK_ERROR"); + } +}; +``` + +**Retry logic**: + +```tsx +const fetchWithRetry = async ( + url: string, + options?: RequestInit, + retries = 3 +) => { + for (let i = 0; i < retries; i++) { + try { + return await fetchWithErrorHandling(url, options); + } catch (error) { + if (i === retries - 1) throw error; + // Exponential backoff + await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000)); + } + } +}; +``` + +--- + +### 4. Authentication + +**Token management**: + +```tsx +import * as SecureStore from "expo-secure-store"; + +const TOKEN_KEY = "auth_token"; + +export const auth = { + getToken: () => SecureStore.getItemAsync(TOKEN_KEY), + setToken: (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token), + removeToken: () => SecureStore.deleteItemAsync(TOKEN_KEY), +}; + +// Authenticated fetch wrapper +const authFetch = async (url: string, options: RequestInit = {}) => { + const token = await auth.getToken(); + + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: token ? `Bearer ${token}` : "", + }, + }); +}; +``` + +**Token refresh**: + +```tsx +let isRefreshing = false; +let refreshPromise: Promise | null = null; + +const getValidToken = async (): Promise => { + const token = await auth.getToken(); + + if (!token || isTokenExpired(token)) { + if (!isRefreshing) { + isRefreshing = true; + refreshPromise = refreshToken().finally(() => { + isRefreshing = false; + refreshPromise = null; + }); + } + return refreshPromise!; + } + + return token; +}; +``` + +--- + +### 5. Offline Support + +**Check network status**: + +```tsx +import NetInfo from "@react-native-community/netinfo"; + +// Hook for network status +function useNetworkStatus() { + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener((state) => { + setIsOnline(state.isConnected ?? true); + }); + }, []); + + return isOnline; +} +``` + +**Offline-first with React Query**: + +```tsx +import { onlineManager } from "@tanstack/react-query"; +import NetInfo from "@react-native-community/netinfo"; + +// Sync React Query with network status +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(state.isConnected ?? true); + }); +}); + +// Queries will pause when offline and resume when online +``` + +--- + +### 6. Environment Variables + +**Using environment variables for API configuration**: + +Expo supports environment variables with the `EXPO_PUBLIC_` prefix. These are inlined at build time and available in your JavaScript code. + +```tsx +// .env +EXPO_PUBLIC_API_URL=https://api.example.com +EXPO_PUBLIC_API_VERSION=v1 + +// Usage in code +const API_URL = process.env.EXPO_PUBLIC_API_URL; + +const fetchUsers = async () => { + const response = await fetch(`${API_URL}/users`); + return response.json(); +}; +``` + +**Environment-specific configuration**: + +```tsx +// .env.development +EXPO_PUBLIC_API_URL=http://localhost:3000 + +// .env.production +EXPO_PUBLIC_API_URL=https://api.production.com +``` + +**Creating an API client with environment config**: + +```tsx +// api/client.ts +const BASE_URL = process.env.EXPO_PUBLIC_API_URL; + +if (!BASE_URL) { + throw new Error("EXPO_PUBLIC_API_URL is not defined"); +} + +export const apiClient = { + get: async (path: string): Promise => { + const response = await fetch(`${BASE_URL}${path}`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }, + + post: async (path: string, body: unknown): Promise => { + const response = await fetch(`${BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }, +}; +``` + +**Important notes**: + +- Only variables prefixed with `EXPO_PUBLIC_` are exposed to the client bundle +- Never put secrets (API keys with write access, database passwords) in `EXPO_PUBLIC_` variables—they're visible in the built app +- Environment variables are inlined at **build time**, not runtime +- Restart the dev server after changing `.env` files +- For server-side secrets in API routes, use variables without the `EXPO_PUBLIC_` prefix + +**TypeScript support**: + +```tsx +// types/env.d.ts +declare global { + namespace NodeJS { + interface ProcessEnv { + EXPO_PUBLIC_API_URL: string; + EXPO_PUBLIC_API_VERSION?: string; + } + } +} + +export {}; +``` + +--- + +### 7. Request Cancellation + +**Cancel on unmount**: + +```tsx +useEffect(() => { + const controller = new AbortController(); + + fetch(url, { signal: controller.signal }) + .then((response) => response.json()) + .then(setData) + .catch((error) => { + if (error.name !== "AbortError") { + setError(error); + } + }); + + return () => controller.abort(); +}, [url]); +``` + +**With React Query** (automatic): + +```tsx +// React Query automatically cancels requests when queries are invalidated +// or components unmount +``` + +--- + +## Decision Tree + +``` +User asks about networking + |-- Route-level data loading (web, SDK 55+)? + | \-- Expo Router loaders — see references/expo-router-loaders.md + | + |-- Basic fetch? + | \-- Use fetch API with error handling + | + |-- Need caching/state management? + | |-- Complex app -> React Query (TanStack Query) + | \-- Simpler needs -> SWR or custom hooks + | + |-- Authentication? + | |-- Token storage -> expo-secure-store + | \-- Token refresh -> Implement refresh flow + | + |-- Error handling? + | |-- Network errors -> Check connectivity first + | |-- HTTP errors -> Parse response, throw typed errors + | \-- Retries -> Exponential backoff + | + |-- Offline support? + | |-- Check status -> NetInfo + | \-- Queue requests -> React Query persistence + | + |-- Environment/API config? + | |-- Client-side URLs -> EXPO_PUBLIC_ prefix in .env + | |-- Server secrets -> Non-prefixed env vars (API routes only) + | \-- Multiple environments -> .env.development, .env.production + | + \-- Performance? + |-- Caching -> React Query with staleTime + |-- Deduplication -> React Query handles this + \-- Cancellation -> AbortController or React Query +``` + +## Common Mistakes + +**Wrong: No error handling** + +```tsx +const data = await fetch(url).then((r) => r.json()); +``` + +**Right: Check response status** + +```tsx +const response = await fetch(url); +if (!response.ok) throw new Error(`HTTP ${response.status}`); +const data = await response.json(); +``` + +**Wrong: Storing tokens in AsyncStorage** + +```tsx +await AsyncStorage.setItem("token", token); // Not secure! +``` + +**Right: Use SecureStore for sensitive data** + +```tsx +await SecureStore.setItemAsync("token", token); +``` + +## Example Invocations + +User: "How do I make API calls in React Native?" +-> Use fetch, wrap with error handling + +User: "Should I use React Query or SWR?" +-> React Query for complex apps, SWR for simpler needs + +User: "My app needs to work offline" +-> Use NetInfo for status, React Query persistence for caching + +User: "How do I handle authentication tokens?" +-> Store in expo-secure-store, implement refresh flow + +User: "API calls are slow" +-> Check caching strategy, use React Query staleTime + +User: "How do I configure different API URLs for dev and prod?" +-> Use EXPO*PUBLIC* env vars with .env.development and .env.production files + +User: "Where should I put my API key?" +-> Client-safe keys: EXPO*PUBLIC* in .env. Secret keys: non-prefixed env vars in API routes only + +User: "How do I load data for a page in Expo Router?" +-> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch. diff --git a/plugins/expo/skills/native-data-fetching/agents/openai.yaml b/plugins/expo/skills/native-data-fetching/agents/openai.yaml new file mode 100644 index 00000000..f28d22dd --- /dev/null +++ b/plugins/expo/skills/native-data-fetching/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Native Data Fetching" + short_description: "Implement and debug Expo network requests, caching, auth, offline support, and Expo Router data loaders" + default_prompt: "Use $native-data-fetching for Expo data fetching work: fetch wrappers, React Query or SWR setup, error handling, retry/cancellation, authenticated requests, environment-based API URLs, offline behavior, and Expo Router loaders." diff --git a/plugins/expo/skills/native-data-fetching/references/expo-router-loaders.md b/plugins/expo/skills/native-data-fetching/references/expo-router-loaders.md new file mode 100644 index 00000000..ca3942c4 --- /dev/null +++ b/plugins/expo/skills/native-data-fetching/references/expo-router-loaders.md @@ -0,0 +1,341 @@ +# Expo Router Data Loaders + +Route-level data loading for web apps using Expo SDK 55+. Loaders are async functions exported from route files that load data before the route renders, following the Remix/React Router loader model. + +**Dual execution model:** + +- **Initial page load (SSR):** The loader runs server-side. Its return value is serialized as JSON and embedded in the HTML response. +- **Client-side navigation:** The browser fetches the loader data from the server via HTTP. The route renders once the data arrives. + +You write one function and the framework manages when and how it executes. + +## Configuration + +**Requirements:** Expo SDK 55+, web output mode (`npx expo serve` or `npx expo export --platform web`) set in `app.json` or `app.config.js`. + +**Server rendering:** + +```json +{ + "expo": { + "web": { + "output": "server" + }, + "plugins": [ + ["expo-router", { + "unstable_useServerDataLoaders": true, + "unstable_useServerRendering": true + }] + ] + } +} +``` + +**Static/SSG:** + +```json +{ + "expo": { + "web": { + "output": "static" + }, + "plugins": [ + ["expo-router", { + "unstable_useServerDataLoaders": true + }] + ] + } +} +``` + +| | `"server"` | `"static"` | +|---|-----------|------------| +| `unstable_useServerDataLoaders` | Required | Required | +| `unstable_useServerRendering` | Required | Not required | +| Loader runs on | Live server (every request) | Build time (static generation) | +| `request` object | Full access (headers, cookies) | Not available | +| Hosting | Node.js server (EAS Hosting) | Any static host (Netlify, Vercel, S3) | + +## Imports + +Loaders use two packages: + +- **`expo-router`** — `useLoaderData` hook +- **`expo-server`** — `LoaderFunction` type, `StatusError`, `setResponseHeaders`. Always available (dependency of `expo-router`), no install needed. + +## Basic Loader + +For loaders without params, a plain async function works: + +```tsx +// app/posts/index.tsx +import { Suspense } from "react"; +import { useLoaderData } from "expo-router"; +import { ActivityIndicator, View, Text } from "react-native"; + +export async function loader() { + const response = await fetch("https://api.example.com/posts"); + const posts = await response.json(); + return { posts }; +} + +function PostList() { + const { posts } = useLoaderData(); + + return ( + + {posts.map((post) => ( + {post.title} + ))} + + ); +} + +export default function Posts() { + return ( + }> + + + ); +} +``` + +`useLoaderData` is typed via `typeof loader` — the generic parameter infers the return type. + +## Dynamic Routes + +For loaders with params, use the `LoaderFunction` type from `expo-server`. The first argument is the request (an immutable `Request`-like object, or `undefined` in static mode). The second is `params` (`Record`), which contains **path parameters only**. Access individual params with a cast like `params.id as string`. For query parameters, use `new URL(request.url).searchParams`: + +```tsx +// app/posts/[id].tsx +import { Suspense } from "react"; +import { useLoaderData } from "expo-router"; +import { StatusError, type LoaderFunction } from "expo-server"; +import { ActivityIndicator, View, Text } from "react-native"; + +type Post = { + id: number; + title: string; + body: string; +}; + +export const loader: LoaderFunction<{ post: Post }> = async ( + request, + params, +) => { + const id = params.id as string; + const response = await fetch(`https://api.example.com/posts/${id}`); + + if (!response.ok) { + throw new StatusError(404, `Post ${id} not found`); + } + + const post: Post = await response.json(); + return { post }; +}; + +function PostContent() { + const { post } = useLoaderData(); + + return ( + + {post.title} + {post.body} + + ); +} + +export default function PostDetail() { + return ( + }> + + + ); +} +``` + +Catch-all routes access `params.slug` the same way: + +```tsx +// app/docs/[...slug].tsx +import { type LoaderFunction } from "expo-server"; + +type Doc = { title: string; content: string }; + +export const loader: LoaderFunction<{ doc: Doc }> = async (request, params) => { + const slug = params.slug as string[]; + const path = slug.join("/"); + const doc = await fetchDoc(path); + return { doc }; +}; +``` + +Query parameters are available via the `request` object (server output mode only): + +```tsx +// app/search.tsx +import { type LoaderFunction } from "expo-server"; + +export const loader: LoaderFunction<{ results: any[]; query: string }> = async (request) => { + // Assuming request.url is `/search?q=expo&page=2` + const url = new URL(request!.url); + const query = url.searchParams.get("q") ?? ""; + const page = Number(url.searchParams.get("page") ?? "1"); + + const results = await fetchSearchResults(query, page); + return { results, query }; +}; +``` + +## Server-Side Secrets & Request Access + +Loaders run on the server, so you can access secrets and server-only resources directly: + +```tsx +// app/dashboard.tsx +import { type LoaderFunction } from "expo-server"; + +export const loader: LoaderFunction<{ balance: any; isAuthenticated: boolean }> = async ( + request, + params, +) => { + const data = await fetch("https://api.stripe.com/v1/balance", { + headers: { + Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, + }, + }); + + const sessionToken = request?.headers.get("cookie")?.match(/session=([^;]+)/)?.[1]; + + const balance = await data.json(); + return { balance, isAuthenticated: !!sessionToken }; +}; +``` + +The `request` object is available in server output mode. In static output mode, `request` is always `undefined`. + +## Response Utilities + +### Setting Response Headers + +```tsx +// app/products.tsx +import { setResponseHeaders } from "expo-server"; + +export async function loader() { + setResponseHeaders({ + "Cache-Control": "public, max-age=300", + }); + + const products = await fetchProducts(); + return { products }; +} +``` + +### Throwing HTTP Errors + +```tsx +// app/products/[id].tsx +import { StatusError, type LoaderFunction } from "expo-server"; + +export const loader: LoaderFunction<{ product: Product }> = async (request, params) => { + const id = params.id as string; + const product = await fetchProduct(id); + + if (!product) { + throw new StatusError(404, "Product not found"); + } + + return { product }; +}; +``` + +## Suspense & Error Boundaries + +### Loading States with Suspense + +`useLoaderData()` suspends during client-side navigation. Push it into a child component and wrap with ``: + +```tsx +// app/posts/index.tsx +import { Suspense } from "react"; +import { useLoaderData } from "expo-router"; +import { ActivityIndicator, View, Text } from "react-native"; + +export async function loader() { + const response = await fetch("https://api.example.com/posts"); + return { posts: await response.json() }; +} + +function PostList() { + const { posts } = useLoaderData(); + + return ( + + {posts.map((post) => ( + {post.title} + ))} + + ); +} + +export default function Posts() { + return ( + + + + } + > + + + ); +} +``` + +The `` boundary must be above the component calling `useLoaderData()`. On initial page load the data is already in the HTML, suspension only occurs during client-side navigation. + +### Error Boundaries + +```tsx +// app/posts/[id].tsx +export function ErrorBoundary({ error }: { error: Error }) { + return ( + + Error: {error.message} + + ); +} +``` + +When a loader throws (including `StatusError`), the nearest `ErrorBoundary` catches it. + +## Static vs Server Rendering + +| | Server (`"server"`) | Static (`"static"`) | +|---|---|---| +| **When loader runs** | Every request (live) | At build time (`npx expo export`) | +| **Data freshness** | Fresh on initial server request | Stale until next build | +| **`request` object** | Full access | Not available | +| **Hosting** | Node.js server (EAS Hosting) | Any static host | +| **Use case** | Personalized/dynamic content | Marketing pages, blogs, docs | + +**Choose server** when data changes frequently or content is personalized (cookies, auth, headers). + +**Choose static** when content is the same for all users and changes infrequently. + +## Best Practices + +- Loaders are web-only; use client-side fetching (React Query, fetch) for native +- Loaders cannot be used in `_layout` files — only in route files +- Use `LoaderFunction` from `expo-server` to type loaders that use params +- The request object is immutable — use optional chaining (`request?.headers`) as it may be `undefined` in static mode +- Return only JSON-serializable values (no `Date`, `Map`, `Set`, class instances, functions) +- Use non-prefixed `process.env` vars for secrets in loaders, not `EXPO_PUBLIC_` (which is embedded in the client bundle) +- Use `StatusError` from `expo-server` for HTTP error responses +- Use `setResponseHeaders` from `expo-server` to set headers +- Export `ErrorBoundary` from route files to handle loader failures gracefully +- Validate and sanitize user input (params, query strings) before using in database queries or API calls +- Handle errors gracefully with try/catch; log server-side for debugging +- Loader data is currently cached for the session. This is a known limitation that will be lifted in a future release diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md new file mode 100644 index 00000000..f43cb4ef --- /dev/null +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -0,0 +1,133 @@ +--- +name: upgrading-expo +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 (useContext → use, Context.Provider → Context, forwardRef removal) +- ./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/plugins/expo/skills/upgrading-expo/agents/openai.yaml b/plugins/expo/skills/upgrading-expo/agents/openai.yaml new file mode 100644 index 00000000..09c353de --- /dev/null +++ b/plugins/expo/skills/upgrading-expo/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Upgrading Expo" + short_description: "Upgrade Expo SDKs, fix dependencies, adopt React 19 / React Compiler, and migrate deprecated Expo packages" + default_prompt: "Use $upgrading-expo to upgrade an Expo SDK, run diagnostics, fix dependency conflicts, decide whether prebuild/cache clearing applies, and migrate away from deprecated Expo packages." diff --git a/plugins/expo/skills/upgrading-expo/references/expo-av-to-audio.md b/plugins/expo/skills/upgrading-expo/references/expo-av-to-audio.md new file mode 100644 index 00000000..afacde7f --- /dev/null +++ b/plugins/expo/skills/upgrading-expo/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(); +} + +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/plugins/expo/skills/upgrading-expo/references/expo-av-to-video.md b/plugins/expo/skills/upgrading-expo/references/expo-av-to-video.md new file mode 100644 index 00000000..5c9bec19 --- /dev/null +++ b/plugins/expo/skills/upgrading-expo/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