diff --git a/.agents/skills/building-native-ui/SKILL.md b/.agents/skills/building-native-ui/SKILL.md new file mode 100644 index 0000000..9a9df4c --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/animations.md b/.agents/skills/building-native-ui/references/animations.md new file mode 100644 index 0000000..657cad8 --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/controls.md b/.agents/skills/building-native-ui/references/controls.md new file mode 100644 index 0000000..762fe20 --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/form-sheet.md b/.agents/skills/building-native-ui/references/form-sheet.md new file mode 100644 index 0000000..1ed80fb --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/gradients.md b/.agents/skills/building-native-ui/references/gradients.md new file mode 100644 index 0000000..329600d --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/icons.md b/.agents/skills/building-native-ui/references/icons.md new file mode 100644 index 0000000..eebf674 --- /dev/null +++ b/.agents/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/.agents/skills/building-native-ui/references/media.md b/.agents/skills/building-native-ui/references/media.md new file mode 100644 index 0000000..50c0ffb --- /dev/null +++ b/.agents/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' }); + + + {tasks?.map((t) =>
{t.text}
)} + + ); +} +``` + +## Development vs Production + +Always use `npx convex dev` during development. It runs against your personal dev deployment and syncs code on save. + +When ready to ship, deploy to production: + +```bash +npx convex deploy +``` + +This pushes to the production deployment, which is separate from dev. Do not use `deploy` during development. + +## Next Steps + +- Add authentication: use the `convex-setup-auth` skill +- Design your schema: see [Schema docs](https://docs.convex.dev/database/schemas) +- Build components: use the `convex-create-component` skill +- Plan a migration: use the `convex-migration-helper` skill +- Add file storage: see [File Storage docs](https://docs.convex.dev/file-storage) +- Set up cron jobs: see [Scheduling docs](https://docs.convex.dev/scheduling) + +## Checklist + +- [ ] Determined starting point: new project or existing app +- [ ] If new project: scaffolded with `npm create convex@latest` using appropriate template +- [ ] If existing app: installed `convex` and wired up the provider +- [ ] User has `npx convex dev` running and connected to a deployment +- [ ] `convex/_generated/` directory exists with types +- [ ] `.env.local` has the deployment URL +- [ ] Verified a basic query/mutation round-trip works diff --git a/.agents/skills/convex-quickstart/agents/openai.yaml b/.agents/skills/convex-quickstart/agents/openai.yaml new file mode 100644 index 0000000..a51a6d0 --- /dev/null +++ b/.agents/skills/convex-quickstart/agents/openai.yaml @@ -0,0 +1,10 @@ +interface: + display_name: "Convex Quickstart" + short_description: "Start a new Convex app or add Convex to an existing frontend." + icon_small: "./assets/icon.svg" + icon_large: "./assets/icon.svg" + brand_color: "#F97316" + default_prompt: "Set up Convex for this project as fast as possible. First decide whether this is a new app or an existing app, then scaffold or integrate Convex and verify the setup works." + +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/convex-quickstart/assets/icon.svg b/.agents/skills/convex-quickstart/assets/icon.svg new file mode 100644 index 0000000..d83a73f --- /dev/null +++ b/.agents/skills/convex-quickstart/assets/icon.svg @@ -0,0 +1,4 @@ + diff --git a/.agents/skills/convex-setup-auth/SKILL.md b/.agents/skills/convex-setup-auth/SKILL.md new file mode 100644 index 0000000..5c0c994 --- /dev/null +++ b/.agents/skills/convex-setup-auth/SKILL.md @@ -0,0 +1,113 @@ +--- +name: convex-setup-auth +description: Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows. +--- + +# Convex Authentication Setup + +Implement secure authentication in Convex with user management and access control. + +## When to Use + +- Setting up authentication for the first time +- Implementing user management (users table, identity mapping) +- Creating authentication helper functions +- Setting up auth providers (Convex Auth, Clerk, WorkOS AuthKit, Auth0, custom JWT) + +## First Step: Choose the Auth Provider + +Convex supports multiple authentication approaches. Do not assume a provider. + +Before writing setup code: + +1. Ask the user which auth solution they want, unless the repository already makes it obvious +2. If the repo already uses a provider, continue with that provider unless the user wants to switch +3. If the user has not chosen a provider and the repo does not make it obvious, ask before proceeding + +Common options: + +- [Convex Auth](https://docs.convex.dev/auth/convex-auth) - good default when the user wants auth handled directly in Convex +- [Clerk](https://docs.convex.dev/auth/clerk) - use when the app already uses Clerk or the user wants Clerk's hosted auth features +- [WorkOS AuthKit](https://docs.convex.dev/auth/authkit/) - use when the app already uses WorkOS or the user wants AuthKit specifically +- [Auth0](https://docs.convex.dev/auth/auth0) - use when the app already uses Auth0 +- Custom JWT provider - use when integrating an existing auth system not covered above + +Look for signals in the repo before asking: + +- Dependencies such as `@clerk/*`, `@workos-inc/*`, `@auth0/*`, or Convex Auth packages +- Existing files such as `convex/auth.config.ts`, auth middleware, provider wrappers, or login components +- Environment variables that clearly point at a provider + +## After Choosing a Provider + +Read the provider's official guide and the matching local reference file: + +- Convex Auth: [official docs](https://docs.convex.dev/auth/convex-auth), then `references/convex-auth.md` +- Clerk: [official docs](https://docs.convex.dev/auth/clerk), then `references/clerk.md` +- WorkOS AuthKit: [official docs](https://docs.convex.dev/auth/authkit/), then `references/workos-authkit.md` +- Auth0: [official docs](https://docs.convex.dev/auth/auth0), then `references/auth0.md` + +The local reference files contain the concrete workflow, expected files and env vars, gotchas, and validation checks. + +Use those sources for: + +- package installation +- client provider wiring +- environment variables +- `convex/auth.config.ts` setup +- login and logout UI patterns +- framework-specific setup for React, Vite, or Next.js + +For shared auth behavior, use the official Convex docs as the source of truth: + +- [Auth in Functions](https://docs.convex.dev/auth/functions-auth) for `ctx.auth.getUserIdentity()` +- [Storing Users in the Convex Database](https://docs.convex.dev/auth/database-auth) for optional app-level user storage +- [Authentication](https://docs.convex.dev/auth) for general auth and authorization guidance +- [Convex Auth Authorization](https://labs.convex.dev/auth/authz) when the provider is Convex Auth + +Do not invent a provider-agnostic user sync pattern from memory. +For third-party providers, only add app-level user storage if the app actually needs user documents in Convex. +For Convex Auth, do not add a parallel `users` table plus `storeUser` flow. Follow the Convex Auth docs and built-in auth tables instead. + +Do not invent provider-specific setup from memory when the docs are available. +Do not assume provider initialization commands finish the entire integration. Verify generated files and complete the post-init wiring steps the provider reference calls out. + +## Workflow + +1. Determine the provider, either by asking the user or inferring from the repo +2. Ask whether the user wants local-only setup or production-ready setup now +3. Read the matching provider reference file +4. Follow the official provider docs for current setup details +5. Follow the official Convex docs for shared backend auth behavior, user storage, and authorization patterns +6. Only add app-level user storage if the docs and app requirements call for it +7. Add authorization checks for ownership, roles, or team access only where the app needs them +8. Verify login state, protected queries, environment variables, and production configuration if requested + +If the flow blocks on interactive provider or deployment setup, ask the user explicitly for the exact human step needed, then continue after they complete it. +For UI-facing auth flows, offer to validate the real sign-up or sign-in flow after setup is done. +If the environment has browser automation tools, you can use them. +If it does not, give the user a short manual validation checklist instead. + +## Reference Files + +### Provider References + +- `references/convex-auth.md` +- `references/clerk.md` +- `references/workos-authkit.md` +- `references/auth0.md` + +## Checklist + +- [ ] Chosen the correct auth provider before writing setup code +- [ ] Read the relevant provider reference file +- [ ] Asked whether the user wants local-only setup or production-ready setup +- [ ] Used the official provider docs for provider-specific wiring +- [ ] Used the official Convex docs for shared auth behavior and authorization patterns +- [ ] Only added app-level user storage if the app actually needs it +- [ ] Did not invent a cross-provider `users` table or `storeUser` flow for Convex Auth +- [ ] Added authentication checks in protected backend functions +- [ ] Added authorization checks where the app actually needs them +- [ ] Clear error messages ("Not authenticated", "Unauthorized") +- [ ] Client auth provider configured for the chosen provider +- [ ] If requested, production auth setup is covered too diff --git a/.agents/skills/convex-setup-auth/agents/openai.yaml b/.agents/skills/convex-setup-auth/agents/openai.yaml new file mode 100644 index 0000000..d1c90a1 --- /dev/null +++ b/.agents/skills/convex-setup-auth/agents/openai.yaml @@ -0,0 +1,10 @@ +interface: + display_name: "Convex Setup Auth" + short_description: "Set up Convex auth, user identity mapping, and access control." + icon_small: "./assets/icon.svg" + icon_large: "./assets/icon.svg" + brand_color: "#2563EB" + default_prompt: "Set up authentication for this Convex app. Figure out the provider first, then wire up the user model, identity mapping, and access control with the smallest solid implementation." + +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/convex-setup-auth/assets/icon.svg b/.agents/skills/convex-setup-auth/assets/icon.svg new file mode 100644 index 0000000..4917dbb --- /dev/null +++ b/.agents/skills/convex-setup-auth/assets/icon.svg @@ -0,0 +1,3 @@ + diff --git a/.agents/skills/convex-setup-auth/references/auth0.md b/.agents/skills/convex-setup-auth/references/auth0.md new file mode 100644 index 0000000..9c729c5 --- /dev/null +++ b/.agents/skills/convex-setup-auth/references/auth0.md @@ -0,0 +1,116 @@ +# Auth0 + +Official docs: + +- https://docs.convex.dev/auth/auth0 +- https://auth0.github.io/auth0-cli/ +- https://auth0.github.io/auth0-cli/auth0_apps_create.html + +Use this when the app already uses Auth0 or the user wants Auth0 specifically. + +## Workflow + +1. Confirm the user wants Auth0 +2. Determine the app framework and whether Auth0 is already partly set up +3. Ask whether the user wants local-only setup or production-ready setup now +4. Read the official Convex and Auth0 guides before making changes +5. Ask whether they want the fastest setup path by installing the Auth0 CLI +6. If they agree, install the Auth0 CLI and do as much of the Auth0 app setup as possible through the CLI +7. If they do not want the CLI path, use the Auth0 dashboard path instead +8. Complete the relevant Auth0 frontend quickstart if the app does not already have Auth0 wired up +9. Configure `convex/auth.config.ts` with the Auth0 domain and client ID +10. Set environment variables for local and production environments +11. Wrap the app with `Auth0Provider` and `ConvexProviderWithAuth0` +12. Gate Convex-backed UI with Convex auth state +13. Try to verify Convex reports the user as authenticated after Auth0 login +14. If the refresh-token path fails, stop improvising and send the user back to the official docs +15. If the user wants production-ready setup, make sure the production Auth0 tenant and env vars are also covered + +## What To Do + +- Read the official Convex and Auth0 guide before writing setup code +- Prefer the Auth0 CLI path for mechanical setup if the user is willing to install it, but do not present it as a fully validated end-to-end path yet +- Ask the user directly: "The fastest path is to install the Auth0 CLI so I can do more of this for you. If you want, I can install it and then only ask you to log in when needed. Would you like me to do that?" +- Make sure the app has already completed the relevant Auth0 quickstart for its frontend +- Use the official examples for `Auth0Provider` and `ConvexProviderWithAuth0` +- If the Auth0 login or refresh flow starts failing in a way that is not clearly explained by the docs, say that plainly and fall back to the official docs instead of pretending the flow is validated + +## Key Setup Areas + +- install the Auth0 SDK for the app's framework +- configure `convex/auth.config.ts` with the Auth0 domain and client ID +- set environment variables for local and production environments +- wrap the app with `Auth0Provider` and `ConvexProviderWithAuth0` +- use Convex auth state when gating Convex-backed UI + +## Files and Env Vars To Expect + +- `convex/auth.config.ts` +- frontend app entry or provider wrapper +- Auth0 CLI install docs: `https://auth0.github.io/auth0-cli/` +- Auth0 environment variables commonly include: + - `AUTH0_DOMAIN` + - `AUTH0_CLIENT_ID` + - `VITE_AUTH0_DOMAIN` + - `VITE_AUTH0_CLIENT_ID` + +## Concrete Steps + +1. Start by reading `https://docs.convex.dev/auth/auth0` and the relevant Auth0 quickstart for the app's framework +2. Ask whether the user wants the Auth0 CLI path +3. If yes, install Auth0 CLI and have the user authenticate it with `auth0 login` +4. Use `auth0 apps create` with SPA settings, callback URL, logout URL, and web origins if creating a new app +5. If not using the CLI path, complete the relevant Auth0 frontend quickstart and create the Auth0 app in the dashboard +6. Get the Auth0 domain and client ID from the CLI output or the Auth0 dashboard +7. Install the Auth0 SDK for the app's framework +8. Create or update `convex/auth.config.ts` with the Auth0 domain and client ID +9. Set frontend and backend environment variables +10. Wrap the app in `Auth0Provider` +11. Replace plain `ConvexProvider` wiring with `ConvexProviderWithAuth0` +12. Run the normal Convex dev or deploy flow after backend config changes +13. Try the official provider config shown in the Convex docs +14. If login works but Convex auth or token refresh fails in a way you cannot clearly resolve, stop and tell the user to follow the official docs manually for now +15. Only claim success if the user can sign in and Convex recognizes the authenticated session +16. If the user wants production-ready setup, configure the production Auth0 tenant values and production environment variables too + +## Gotchas + +- The Convex docs assume the Auth0 side is already set up, so do not skip the Auth0 quickstart if the app is starting from scratch +- The Auth0 CLI is often the fastest path for a fresh setup, but it still requires the user to authenticate the CLI to their Auth0 tenant +- If the user agrees to install the Auth0 CLI, do the mechanical setup yourself instead of bouncing them through the dashboard +- If login succeeds but Convex still reports unauthenticated, double-check `convex/auth.config.ts` and whether the backend config was synced +- We were able to automate Auth0 app creation and Convex config wiring, but we did not fully validate the refresh-token path end to end +- In validation, the documented `useRefreshTokens={true}` and `cacheLocation="localstorage"` setup hit refresh-token failures, so do not present that path as settled +- If you hit Auth0 errors like `Unknown or invalid refresh token`, do not keep inventing fixes indefinitely, send the user back to the official docs and explain that this path is still under investigation +- Keep dev and prod tenants separate if the project uses different Auth0 environments +- Do not confuse "Auth0 login works" with "Convex can validate the Auth0 token". Both need to work. +- If the repo already uses Auth0, preserve existing redirect and tenant configuration unless the user asked to change it. +- Do not assume the local Auth0 tenant settings match production. Verify the production domain, client ID, and callback URLs separately. +- For local dev, make sure the Auth0 app settings match the app's real local port for callback URLs, logout URLs, and web origins + +## Production + +- Ask whether the user wants dev-only setup or production-ready setup +- If the answer is production-ready, make sure the production Auth0 tenant values, callback URLs, and Convex deployment config are all covered +- Verify production environment variables and redirect settings before calling the task complete +- Do not silently write a notes file into the repo by default. If the user wants rollout or handoff docs, create one explicitly. + +## Validation + +- Verify the user can complete the Auth0 login flow +- Verify Convex-authenticated UI renders only after Convex auth state is ready +- Verify protected Convex queries succeed after login +- Verify `ctx.auth.getUserIdentity()` is non-null in protected backend functions +- Verify the Auth0 app settings match the real local callback and logout URLs during development +- If the Auth0 refresh-token path fails, mark the setup as not fully validated and direct the user to the official docs instead of claiming the skill completed successfully +- If production-ready setup was requested, verify the production Auth0 configuration is also covered + +## Checklist + +- [ ] Confirm the user wants Auth0 +- [ ] Ask whether the user wants local-only setup or production-ready setup +- [ ] Complete the relevant Auth0 frontend setup +- [ ] Configure `convex/auth.config.ts` +- [ ] Set environment variables +- [ ] Verify Convex authenticated state after login, or explicitly tell the user this path is still under investigation and send them to the official docs +- [ ] If requested, configure the production deployment too diff --git a/.agents/skills/convex-setup-auth/references/clerk.md b/.agents/skills/convex-setup-auth/references/clerk.md new file mode 100644 index 0000000..7dbde19 --- /dev/null +++ b/.agents/skills/convex-setup-auth/references/clerk.md @@ -0,0 +1,113 @@ +# Clerk + +Official docs: + +- https://docs.convex.dev/auth/clerk +- https://clerk.com/docs/guides/development/integrations/databases/convex + +Use this when the app already uses Clerk or the user wants Clerk's hosted auth features. + +## Workflow + +1. Confirm the user wants Clerk +2. Make sure the user has a Clerk account and a Clerk application +3. Determine the app framework: + - React + - Next.js + - TanStack Start +4. Ask whether the user wants local-only setup or production-ready setup now +5. Gather the Clerk keys and the Clerk Frontend API URL +6. Follow the correct framework section in the official docs +7. Complete the backend and client wiring +8. Verify Convex reports the user as authenticated after login +9. If the user wants production-ready setup, make sure the production Clerk config is also covered + +## What To Do + +- Read the official Convex and Clerk guide before writing setup code +- If the user does not already have Clerk set up, send them to `https://dashboard.clerk.com/sign-up` to create an account and `https://dashboard.clerk.com/apps/new` to create an application +- Send the user to `https://dashboard.clerk.com/apps/setup/convex` if the Convex integration is not already active +- Match the guide to the app's framework, usually React, Next.js, or TanStack Start +- Use the official examples for `ConvexProviderWithClerk`, `ClerkProvider`, and `useAuth` + +## Key Setup Areas + +- install the Clerk SDK for the framework in use +- configure `convex/auth.config.ts` with the Clerk issuer domain +- set the required Clerk environment variables +- wrap the app with `ClerkProvider` and `ConvexProviderWithClerk` +- use Convex auth-aware UI patterns such as `Authenticated`, `Unauthenticated`, and `AuthLoading` + +## Files and Env Vars To Expect + +- `convex/auth.config.ts` +- React or Vite client entry such as `src/main.tsx` +- Next.js client wrapper for Convex if using App Router +- Clerk account sign-up page: `https://dashboard.clerk.com/sign-up` +- Clerk app creation page: `https://dashboard.clerk.com/apps/new` +- Clerk Convex integration page: `https://dashboard.clerk.com/apps/setup/convex` +- Clerk API keys page: `https://dashboard.clerk.com/last-active?path=api-keys` +- Clerk environment variables: + - `CLERK_JWT_ISSUER_DOMAIN` for Convex backend validation in the Convex docs + - `CLERK_FRONTEND_API_URL` in the Clerk docs + - `VITE_CLERK_PUBLISHABLE_KEY` for Vite apps + - `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` for Next.js apps + - `CLERK_SECRET_KEY` for Next.js server-side Clerk setup where required + +`CLERK_JWT_ISSUER_DOMAIN` and `CLERK_FRONTEND_API_URL` refer to the same Clerk Frontend API URL value. Do not treat them as two different URLs. + +## Concrete Steps + +1. If needed, create a Clerk account at `https://dashboard.clerk.com/sign-up` +2. If needed, create a Clerk application at `https://dashboard.clerk.com/apps/new` +3. Open `https://dashboard.clerk.com/last-active?path=api-keys` and copy the publishable key, plus the secret key for Next.js where needed +4. Open `https://dashboard.clerk.com/apps/setup/convex` +5. Activate the Convex integration in Clerk if it is not already active +6. Copy the Clerk Frontend API URL shown there +7. Install the Clerk package for the app's framework +8. Create or update `convex/auth.config.ts` so Convex validates Clerk tokens +9. Set the publishable key in the frontend environment +10. Set the issuer domain or Frontend API URL so Convex can validate the JWT +11. Replace plain `ConvexProvider` wiring with `ConvexProviderWithClerk` +12. Wrap the app in `ClerkProvider` +13. Use Convex auth helpers for authenticated rendering +14. Run the normal Convex dev or deploy flow after updating backend auth config +15. If the user wants production-ready setup, configure the production Clerk values and production issuer domain too + +## Gotchas + +- Prefer `useConvexAuth()` over raw Clerk auth state when deciding whether Convex-authenticated UI can render +- For Next.js, keep server and client boundaries in mind when creating the Convex provider wrapper +- After changing `convex/auth.config.ts`, run the normal Convex dev or deploy flow so the backend picks up the new config +- Do not stop at "Clerk login works". The important check is that Convex also sees the session and can authenticate requests. +- If the repo already uses Clerk, preserve its existing auth flow unless the user asked to change it. +- Do not assume the same Clerk values work for both dev and production. Check the production issuer domain and publishable key separately. +- The Convex setup page is where you get the Clerk Frontend API URL for Convex. Keep using the Clerk API keys page for the publishable key and the secret key. +- If Convex says no auth provider matched the token, first confirm the Clerk Convex integration was activated at `https://dashboard.clerk.com/apps/setup/convex` +- After activating the Clerk Convex integration, sign out completely and sign back in before retesting. An old Clerk session can keep using a token that Convex rejects. + +## Production + +- Ask whether the user wants dev-only setup or production-ready setup +- If the answer is production-ready, make sure production Clerk keys and issuer configuration are included +- Verify production redirect URLs and any production Clerk domain values before calling the task complete +- Do not silently write a notes file into the repo by default. If the user wants rollout or handoff docs, create one explicitly. + +## Validation + +- Verify the user can sign in with Clerk +- If the Clerk integration was just activated, verify after a full Clerk sign-out and fresh sign-in +- Verify `useConvexAuth()` reaches the authenticated state after Clerk login +- Verify protected Convex queries run successfully inside authenticated UI +- Verify `ctx.auth.getUserIdentity()` is non-null in protected backend functions +- If production-ready setup was requested, verify the production Clerk configuration is also covered + +## Checklist + +- [ ] Confirm the user wants Clerk +- [ ] Ask whether the user wants local-only setup or production-ready setup +- [ ] Follow the correct framework section in the official guide +- [ ] Set Clerk environment variables +- [ ] Configure `convex/auth.config.ts` +- [ ] Verify Convex authenticated state after login +- [ ] If requested, configure the production deployment too diff --git a/.agents/skills/convex-setup-auth/references/convex-auth.md b/.agents/skills/convex-setup-auth/references/convex-auth.md new file mode 100644 index 0000000..d4824d2 --- /dev/null +++ b/.agents/skills/convex-setup-auth/references/convex-auth.md @@ -0,0 +1,143 @@ +# Convex Auth + +Official docs: https://docs.convex.dev/auth/convex-auth +Setup guide: https://labs.convex.dev/auth/setup + +Use this when the user wants auth handled directly in Convex rather than through a third-party provider. + +## Workflow + +1. Confirm the user wants Convex Auth specifically +2. Determine which sign-in methods the app needs: + - magic links or OTPs + - OAuth providers + - passwords and password reset +3. Ask whether the user wants local-only setup or production-ready setup now +4. Read the Convex Auth setup guide before writing code +5. Make sure the project has a configured Convex deployment: + - run `npx convex dev` first if `CONVEX_DEPLOYMENT` is not set + - if CLI configuration requires interactive human input, stop and ask the user to complete that step before continuing +6. Install the auth packages: + - `npm install @convex-dev/auth @auth/core@0.37.0` +7. Run the initialization command: + - `npx @convex-dev/auth` +8. Confirm the initializer created: + - `convex/auth.config.ts` + - `convex/auth.ts` + - `convex/http.ts` +9. Add the required `authTables` to `convex/schema.ts` +10. Replace plain `ConvexProvider` wiring with `ConvexAuthProvider` +11. Configure at least one auth method in `convex/auth.ts` +12. Run `npx convex dev --once` or the normal dev flow to push the updated schema and generated code +13. Verify the client can sign in successfully +14. Verify Convex receives authenticated identity in backend functions +15. If the user wants production-ready setup, make sure the same auth setup is configured for the production deployment as well +16. Only add a `users` table and `storeUser` flow if the app needs app-level user records inside Convex + +## What This Reference Is For + +- choosing Convex Auth as the default provider for a new Convex app +- understanding whether the app wants magic links, OTPs, OAuth, or passwords +- keeping the setup provider-specific while using the official Convex Auth docs for identity and authorization behavior + +## What To Do + +- Read the Convex Auth setup guide before writing setup code +- Follow the setup flow from the docs rather than recreating it from memory +- If the app is new, consider starting from the official starter flow instead of hand-wiring everything +- Treat `npx @convex-dev/auth` as a required initialization step for existing apps, not an optional extra + +## Concrete Steps + +1. Install `@convex-dev/auth` and `@auth/core@0.37.0` +2. Run `npx convex dev` if the project does not already have a configured deployment +3. If `npx convex dev` blocks on interactive setup, ask the user explicitly to finish configuring the Convex deployment +4. Run `npx @convex-dev/auth` +5. Confirm the generated auth setup is present before continuing: + - `convex/auth.config.ts` + - `convex/auth.ts` + - `convex/http.ts` +6. Add `authTables` to `convex/schema.ts` +7. Replace `ConvexProvider` with `ConvexAuthProvider` in the app entry +8. Configure the selected auth methods in `convex/auth.ts` +9. Run `npx convex dev --once` or the normal dev flow so the updated schema and auth files are pushed +10. Verify login locally +11. If the user wants production-ready setup, repeat the required auth configuration against the production deployment + +## Expected Files and Decisions + +- `convex/schema.ts` +- frontend app entry such as `src/main.tsx` or the framework-equivalent provider file +- generated Convex Auth setup produced by `npx @convex-dev/auth` +- an existing configured Convex deployment, or the ability to create one with `npx convex dev` +- `convex/auth.ts` starts with `providers: []` until the app configures actual sign-in methods + +- Decide whether the user is creating a new app or adding auth to an existing app +- For a new app, prefer the official starter flow instead of rebuilding setup by hand +- Decide which auth methods the app needs: + - magic links or OTPs + - OAuth providers + - passwords +- Decide whether the user wants local-only setup or production-ready setup now +- Decide whether the app actually needs a `users` table inside Convex, or whether provider identity alone is enough + +## Gotchas + +- Do not assume a specific sign-in method. Ask which methods the app needs before wiring UI and backend behavior. +- `npx @convex-dev/auth` is important because it initializes the auth setup, including the key material. Do not skip it when adding Convex Auth to an existing project. +- `npx @convex-dev/auth` will fail if the project does not already have a configured `CONVEX_DEPLOYMENT`. +- `npx convex dev` may require interactive setup for deployment creation or project selection. If that happens, ask the user explicitly for that human step instead of guessing. +- `npx @convex-dev/auth` does not finish the whole integration by itself. You still need to add `authTables`, swap in `ConvexAuthProvider`, and configure at least one auth method. +- A project can still build even if `convex/auth.ts` still has `providers: []`, so do not treat a successful build as proof that sign-in is fully configured. +- Convex Auth does not mean every app needs a `users` table. If the app only needs authentication gates, `ctx.auth.getUserIdentity()` may be enough. +- If the app is greenfield, starting from the official starter flow is usually better than partially recreating it by hand. +- Do not stop at local dev setup if the user expects production-ready auth. The production deployment needs the auth setup too. +- Keep provider-specific setup and Convex Auth authorization behavior in the official docs instead of inventing shared patterns from memory. + +## Production + +- Ask whether the user wants dev-only setup or production-ready setup +- If the answer is production-ready, make sure the auth configuration is applied to the production deployment, not just the dev deployment +- Verify production-specific redirect URLs, auth method configuration, and deployment settings before calling the task complete +- Do not silently write a notes file into the repo by default. If the user wants rollout or handoff docs, create one explicitly. + +## Human Handoff + +If `npx convex dev` or deployment setup requires human input: + +- stop and explain exactly what the user needs to do +- say why that step is required +- resume the auth setup immediately after the user confirms it is done + +## Validation + +- Verify the user can complete a sign-in flow +- Offer to validate sign up, sign out, and sign back in with the configured auth method +- If browser automation is available in the environment, you can do this directly +- If browser automation is not available, give the user a short manual validation checklist instead +- Verify `ctx.auth.getUserIdentity()` returns an identity in protected backend functions +- Verify protected UI only renders after Convex-authenticated state is ready +- Verify environment variables and redirect settings match the current app environment +- Verify `convex/auth.ts` no longer has an empty `providers: []` configuration once the app is meant to support real sign-in +- Run `npx convex dev --once` or the normal dev flow after setup changes and confirm Convex codegen and push succeed +- If production-ready setup was requested, verify the production deployment is also configured correctly + +## Checklist + +- [ ] Confirm the user wants Convex Auth specifically +- [ ] Ask whether the user wants local-only setup or production-ready setup +- [ ] Ensure a Convex deployment is configured before running auth initialization +- [ ] Install `@convex-dev/auth` and `@auth/core@0.37.0` +- [ ] Run `npx convex dev` first if needed +- [ ] Run `npx @convex-dev/auth` +- [ ] Confirm `convex/auth.config.ts`, `convex/auth.ts`, and `convex/http.ts` were created +- [ ] Follow the setup guide for package install and wiring +- [ ] Add `authTables` to `convex/schema.ts` +- [ ] Replace `ConvexProvider` with `ConvexAuthProvider` +- [ ] Configure at least one auth method in `convex/auth.ts` +- [ ] Run `npx convex dev --once` or the normal dev flow after setup changes +- [ ] Confirm which sign-in methods the app needs +- [ ] Verify the client can sign in and the backend receives authenticated identity +- [ ] Offer end-to-end validation of sign up, sign out, and sign back in +- [ ] If requested, configure the production deployment too +- [ ] Only add extra `users` table sync if the app needs app-level user records diff --git a/.agents/skills/convex-setup-auth/references/workos-authkit.md b/.agents/skills/convex-setup-auth/references/workos-authkit.md new file mode 100644 index 0000000..038cb9f --- /dev/null +++ b/.agents/skills/convex-setup-auth/references/workos-authkit.md @@ -0,0 +1,114 @@ +# WorkOS AuthKit + +Official docs: + +- https://docs.convex.dev/auth/authkit/ +- https://docs.convex.dev/auth/authkit/add-to-app +- https://docs.convex.dev/auth/authkit/auto-provision + +Use this when the app already uses WorkOS or the user wants AuthKit specifically. + +## Workflow + +1. Confirm the user wants WorkOS AuthKit +2. Determine whether they want: + - a Convex-managed WorkOS team + - an existing WorkOS team +3. Ask whether the user wants local-only setup or production-ready setup now +4. Read the official Convex and WorkOS AuthKit guide +5. Create or update `convex.json` for the app's framework and real local port +6. Follow the correct branch of the setup flow based on that choice +7. Configure the required WorkOS environment variables +8. Configure `convex/auth.config.ts` for WorkOS-issued JWTs +9. Wire the client provider and callback flow +10. Verify authenticated requests reach Convex +11. If the user wants production-ready setup, make sure the production WorkOS configuration is covered too +12. Only add `storeUser` or a `users` table if the app needs first-class user rows inside Convex + +## What To Do + +- Read the official Convex and WorkOS AuthKit guide before writing setup code +- Determine whether the user wants a Convex-managed WorkOS team or an existing WorkOS team +- Treat `convex.json` as a first-class part of the AuthKit setup, not an optional extra +- Follow the current setup flow from the docs instead of relying on older examples + +## Key Setup Areas + +- package installation for the app's framework +- `convex.json` with the `authKit` section for dev, and preview or prod if needed +- environment variables such as `WORKOS_CLIENT_ID`, `WORKOS_API_KEY`, and redirect configuration +- `convex/auth.config.ts` wiring for WorkOS-issued JWTs +- client provider setup and token flow into Convex +- login callback and redirect configuration + +## Files and Env Vars To Expect + +- `convex.json` +- `convex/auth.config.ts` +- frontend auth provider wiring +- callback or redirect route setup where the framework requires it +- WorkOS environment variables commonly include: + - `WORKOS_CLIENT_ID` + - `WORKOS_API_KEY` + - `WORKOS_COOKIE_PASSWORD` + - `VITE_WORKOS_CLIENT_ID` + - `VITE_WORKOS_REDIRECT_URI` + - `NEXT_PUBLIC_WORKOS_REDIRECT_URI` + +For a managed WorkOS team, `convex dev` can provision the AuthKit environment and write local env vars such as `VITE_WORKOS_CLIENT_ID` and `VITE_WORKOS_REDIRECT_URI` into `.env.local` for Vite apps. + +## Concrete Steps + +1. Choose Convex-managed or existing WorkOS team +2. Create or update `convex.json` with the `authKit` section for the framework in use +3. Make sure the dev `redirectUris`, `appHomepageUrl`, `corsOrigins`, and local redirect env vars match the app's actual local port +4. For a managed WorkOS team, run `npx convex dev` and follow the interactive onboarding flow +5. For an existing WorkOS team, get `WORKOS_CLIENT_ID` and `WORKOS_API_KEY` from the WorkOS dashboard and set them with `npx convex env set` +6. Create or update `convex/auth.config.ts` for WorkOS JWT validation +7. Run the normal Convex dev or deploy flow so backend config is synced +8. Wire the WorkOS client provider in the app +9. Configure callback and redirect handling +10. Verify the user can sign in and return to the app +11. Verify Convex sees the authenticated user after login +12. If the user wants production-ready setup, configure the production client ID, API key, redirect URI, and deployment settings too + +## Gotchas + +- The docs split setup between Convex-managed and existing WorkOS teams, so ask which path the user wants if it is not obvious +- Keep dev and prod WorkOS configuration separate where the docs call for different client IDs or API keys +- Only add `storeUser` or a `users` table if the app needs first-class user rows inside Convex +- Do not mix dev and prod WorkOS credentials or redirect URIs +- If the repo already contains WorkOS setup, preserve the current tenant model unless the user wants to change it +- For managed WorkOS setup, `convex dev` is interactive the first time. In non-interactive terminals, stop and ask the user to complete the onboarding prompts. +- `convex.json` is not optional for the managed AuthKit flow. It drives redirect URI, homepage URL, CORS configuration, and local env var generation. +- If the frontend starts on a different port than the one in `convex.json`, the hosted WorkOS sign-in flow will point to the wrong callback URL. Update `convex.json`, update the local redirect env var, and run `npx convex dev` again. +- Vite can fall off `5173` if other apps are already running. Do not assume the default port still matches the generated AuthKit config. +- A successful WorkOS sign-in should redirect back to the local callback route and then reach a Convex-authenticated state. Do not stop at "the hosted WorkOS page loaded." + +## Production + +- Ask whether the user wants dev-only setup or production-ready setup +- If the answer is production-ready, make sure the production WorkOS client ID, API key, redirect URI, and Convex deployment config are all covered +- Verify the production redirect and callback settings before calling the task complete +- Do not silently write a notes file into the repo by default. If the user wants rollout or handoff docs, create one explicitly. + +## Validation + +- Verify the user can complete the login flow and return to the app +- Verify the callback URL matches the real frontend port in local dev +- Verify Convex receives authenticated requests after login +- Verify `convex.json` matches the framework and chosen WorkOS setup path +- Verify `convex/auth.config.ts` matches the chosen WorkOS setup path +- Verify environment variables differ correctly between local and production where needed +- If production-ready setup was requested, verify the production WorkOS configuration is also covered + +## Checklist + +- [ ] Confirm the user wants WorkOS AuthKit +- [ ] Ask whether the user wants local-only setup or production-ready setup +- [ ] Choose Convex-managed or existing WorkOS team +- [ ] Create or update `convex.json` +- [ ] Configure WorkOS environment variables +- [ ] Configure `convex/auth.config.ts` +- [ ] Verify authenticated requests reach Convex after login +- [ ] If requested, configure the production deployment too diff --git a/.agents/skills/expo-cicd-workflows/SKILL.md b/.agents/skills/expo-cicd-workflows/SKILL.md new file mode 100644 index 0000000..48c8a57 --- /dev/null +++ b/.agents/skills/expo-cicd-workflows/SKILL.md @@ -0,0 +1,92 @@ +--- +name: expo-cicd-workflows +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/.agents/skills/expo-cicd-workflows/scripts/fetch.js b/.agents/skills/expo-cicd-workflows/scripts/fetch.js new file mode 100644 index 0000000..466bfc7 --- /dev/null +++ b/.agents/skills/expo-cicd-workflows/scripts/fetch.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { readFile, writeFile, mkdir } 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, { + 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/.agents/skills/expo-cicd-workflows/scripts/package.json b/.agents/skills/expo-cicd-workflows/scripts/package.json new file mode 100644 index 0000000..a3bd716 --- /dev/null +++ b/.agents/skills/expo-cicd-workflows/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/.agents/skills/expo-cicd-workflows/scripts/validate.js b/.agents/skills/expo-cicd-workflows/scripts/validate.js new file mode 100644 index 0000000..bb3d9ff --- /dev/null +++ b/.agents/skills/expo-cicd-workflows/scripts/validate.js @@ -0,0 +1,84 @@ +#!/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) { + const content = await readFile(filePath, 'utf-8'); + + 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); + } + + const schema = await fetchSchema(); + const validator = createValidator(schema); + + 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/.agents/skills/expo-deployment/SKILL.md b/.agents/skills/expo-deployment/SKILL.md new file mode 100644 index 0000000..114aa91 --- /dev/null +++ b/.agents/skills/expo-deployment/SKILL.md @@ -0,0 +1,190 @@ +--- +name: expo-deployment +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 --submit + +# Android: Build and submit to Play Store +npx eas-cli@latest build -p android --profile production --submit + +# Shortcut for iOS TestFlight +npx testflight +``` + +## 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": ">= 16.0.1", + "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 ./reference/testflight.md for credential setup +- See ./reference/ios-app-store.md for App Store submission + +### Android + +- Set up Google Play Console service account +- Configure tracks: internal → closed → open → production +- See ./reference/play-store.md for detailed setup + +### Web + +- EAS Hosting provides preview URLs for PRs +- Production deploys to your custom domain +- See ./reference/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: + platform: ios + profile: production +``` + +See ./reference/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/.agents/skills/expo-deployment/references/app-store-metadata.md b/.agents/skills/expo-deployment/references/app-store-metadata.md new file mode 100644 index 0000000..14a258a --- /dev/null +++ b/.agents/skills/expo-deployment/references/app-store-metadata.md @@ -0,0 +1,479 @@ +# 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/.agents/skills/expo-deployment/references/ios-app-store.md b/.agents/skills/expo-deployment/references/ios-app-store.md new file mode 100644 index 0000000..bc6085b --- /dev/null +++ b/.agents/skills/expo-deployment/references/ios-app-store.md @@ -0,0 +1,355 @@ +# 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": { + "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_API_KEY_ISSUER_ID=xxxxx-xxxx-xxxx-xxxx-xxxxx +EXPO_ASC_API_KEY_ID=XXXXXXXXXX +``` + +### Apple ID Authentication (Alternative) + +For manual submissions, you can use Apple ID: + +```bash +EXPO_APPLE_ID=your@email.com +EXPO_APPLE_TEAM_ID=XXXXXXXXXX +``` + +Note: Requires app-specific password for accounts with 2FA. + +## Submission Commands + +```bash +# Build and submit to App Store Connect +eas build -p ios --profile production --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": ">= 16.0.1", + "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 + +### Automatic Release + +Release immediately when approved: + +```json +{ + "apple": { + "release": { + "automaticRelease": true + } + } +} +``` + +### Scheduled Release + +```json +{ + "apple": { + "release": { + "automaticRelease": "2025-03-01T10:00:00Z" + } + } +} +``` + +### Phased Release + +Gradual rollout over 7 days: + +```json +{ + "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 +- Limited to 3 per Apple Developer account +- 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 ./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: + platform: ios + 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/.agents/skills/expo-deployment/references/play-store.md b/.agents/skills/expo-deployment/references/play-store.md new file mode 100644 index 0000000..88102dd --- /dev/null +++ b/.agents/skills/expo-deployment/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/.agents/skills/expo-deployment/references/testflight.md b/.agents/skills/expo-deployment/references/testflight.md new file mode 100644 index 0000000..e16932a --- /dev/null +++ b/.agents/skills/expo-deployment/references/testflight.md @@ -0,0 +1,58 @@ +# TestFlight + +Always ship to TestFlight first. Internal testers, then external testers, then App Store. Never skip this. + +## Submit + +```bash +npx testflight +``` + +That's it. One command builds and submits to TestFlight. + +## 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"** +Use `autoIncrement: true` in `eas.json`. Problem solved. + +**Credentials issues** +```bash +eas credentials -p ios +``` diff --git a/.agents/skills/expo-deployment/references/workflows.md b/.agents/skills/expo-deployment/references/workflows.md new file mode 100644 index 0000000..f23b6e2 --- /dev/null +++ b/.agents/skills/expo-deployment/references/workflows.md @@ -0,0 +1,200 @@ +# 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: + platform: ios + profile: production + + submit-android: + type: submit + needs: [build-android] + params: + platform: android + profile: production +``` + +## Build on Push + +Trigger builds when pushing to specific branches: + +```yaml +name: Build + +on: + push: + branches: + - main + - release/* + +jobs: + build: + type: build + params: + platform: all + profile: production +``` + +## Conditional Jobs + +Run jobs based on conditions: + +```yaml +name: Conditional Release + +on: + push: + branches: [main] + +jobs: + check-changes: + type: run + params: + command: | + if git diff --name-only HEAD~1 | grep -q "^src/"; then + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + build: + type: build + needs: [check-changes] + if: needs.check-changes.outputs.has_changes == 'true' + params: + platform: all + 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: + platform: ios +``` + +## 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/.agents/skills/expo-dev-client/SKILL.md b/.agents/skills/expo-dev-client/SKILL.md new file mode 100644 index 0000000..84a1cf0 --- /dev/null +++ b/.agents/skills/expo-dev-client/SKILL.md @@ -0,0 +1,164 @@ +--- +name: expo-dev-client +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": ">= 16.0.1", + "appVersionSource": "remote" + }, + "build": { + "production": { + "autoIncrement": true + }, + "development": { + "autoIncrement": true, + "developmentClient": true + } + }, + "submit": { + "production": {}, + "development": {} + } +} +``` + +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: + +```bash +eas build -p ios --profile development --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: `.ipa` file +- Android: `.apk` or `.aab` 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 +eas update +``` diff --git a/.agents/skills/find-skills/SKILL.md b/.agents/skills/find-skills/SKILL.md new file mode 100644 index 0000000..114c663 --- /dev/null +++ b/.agents/skills/find-skills/SKILL.md @@ -0,0 +1,142 @@ +--- +name: find-skills +description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. +--- + +# Find Skills + +This skill helps you discover and install skills from the open agent skills ecosystem. + +## When to Use This Skill + +Use this skill when the user: + +- Asks "how do I do X" where X might be a common task with an existing skill +- Says "find a skill for X" or "is there a skill for X" +- Asks "can you do X" where X is a specialized capability +- Expresses interest in extending agent capabilities +- Wants to search for tools, templates, or workflows +- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) + +## What is the Skills CLI? + +The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. + +**Key commands:** + +- `npx skills find [query]` - Search for skills interactively or by keyword +- `npx skills add ` - Install a skill from GitHub or other sources +- `npx skills check` - Check for skill updates +- `npx skills update` - Update all installed skills + +**Browse skills at:** https://skills.sh/ + +## How to Help Users Find Skills + +### Step 1: Understand What They Need + +When a user asks for help with something, identify: + +1. The domain (e.g., React, testing, design, deployment) +2. The specific task (e.g., writing tests, creating animations, reviewing PRs) +3. Whether this is a common enough task that a skill likely exists + +### Step 2: Check the Leaderboard First + +Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options. + +For example, top skills for web development include: +- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each) +- `anthropics/skills` — Frontend design, document processing (100K+ installs) + +### Step 3: Search for Skills + +If the leaderboard doesn't cover the user's need, run the find command: + +```bash +npx skills find [query] +``` + +For example: + +- User asks "how do I make my React app faster?" → `npx skills find react performance` +- User asks "can you help me with PR reviews?" → `npx skills find pr review` +- User asks "I need to create a changelog" → `npx skills find changelog` + +### Step 4: Verify Quality Before Recommending + +**Do not recommend a skill based solely on search results.** Always verify: + +1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100. +2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors. +3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism. + +### Step 5: Present Options to the User + +When you find relevant skills, present them to the user with: + +1. The skill name and what it does +2. The install count and source +3. The install command they can run +4. A link to learn more at skills.sh + +Example response: + +``` +I found a skill that might help! The "react-best-practices" skill provides +React and Next.js performance optimization guidelines from Vercel Engineering. +(185K installs) + +To install it: +npx skills add vercel-labs/agent-skills@react-best-practices + +Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices +``` + +### Step 6: Offer to Install + +If the user wants to proceed, you can install the skill for them: + +```bash +npx skills add -g -y +``` + +The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. + +## Common Skill Categories + +When searching, consider these common categories: + +| Category | Example Queries | +| --------------- | ---------------------------------------- | +| Web Development | react, nextjs, typescript, css, tailwind | +| Testing | testing, jest, playwright, e2e | +| DevOps | deploy, docker, kubernetes, ci-cd | +| Documentation | docs, readme, changelog, api-docs | +| Code Quality | review, lint, refactor, best-practices | +| Design | ui, ux, design-system, accessibility | +| Productivity | workflow, automation, git | + +## Tips for Effective Searches + +1. **Use specific keywords**: "react testing" is better than just "testing" +2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" +3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` + +## When No Skills Are Found + +If no relevant skills exist: + +1. Acknowledge that no existing skill was found +2. Offer to help with the task directly using your general capabilities +3. Suggest the user could create their own skill with `npx skills init` + +Example: + +``` +I searched for skills related to "xyz" but didn't find any matches. +I can still help you with this task directly! Would you like me to proceed? + +If this is something you do often, you could create your own skill: +npx skills init my-xyz-skill +``` diff --git a/.agents/skills/native-data-fetching/SKILL.md b/.agents/skills/native-data-fetching/SKILL.md new file mode 100644 index 0000000..c516909 --- /dev/null +++ b/.agents/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/.agents/skills/native-data-fetching/references/expo-router-loaders.md b/.agents/skills/native-data-fetching/references/expo-router-loaders.md new file mode 100644 index 0000000..ca3942c --- /dev/null +++ b/.agents/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/.agents/skills/upgrading-expo/SKILL.md b/.agents/skills/upgrading-expo/SKILL.md new file mode 100644 index 0000000..f43cb4e --- /dev/null +++ b/.agents/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/.agents/skills/upgrading-expo/references/expo-av-to-audio.md b/.agents/skills/upgrading-expo/references/expo-av-to-audio.md new file mode 100644 index 0000000..afacde7 --- /dev/null +++ b/.agents/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/.agents/skills/upgrading-expo/references/expo-av-to-video.md b/.agents/skills/upgrading-expo/references/expo-av-to-video.md new file mode 100644 index 0000000..5c9bec1 --- /dev/null +++ b/.agents/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