From b2ea20dfbfc4901ebd07eea79e1f727e26486b39 Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Fri, 20 Mar 2026 07:40:08 +0100 Subject: [PATCH 1/5] ai: Added convex agent skills --- .claude/skills/convex-create-component | 1 + .claude/skills/convex-migration-helper | 1 + .claude/skills/convex-performance-audit | 1 + .claude/skills/convex-quickstart | 1 + .claude/skills/convex-setup-auth | 1 + AGENTS.md | 7 + convex/_generated/ai/ai-files.state.json | 13 + convex/_generated/ai/guidelines.md | 303 +++++++++++++++++++++++ skills-lock.json | 30 +++ 9 files changed, 358 insertions(+) create mode 120000 .claude/skills/convex-create-component create mode 120000 .claude/skills/convex-migration-helper create mode 120000 .claude/skills/convex-performance-audit create mode 120000 .claude/skills/convex-quickstart create mode 120000 .claude/skills/convex-setup-auth create mode 100644 AGENTS.md create mode 100644 convex/_generated/ai/ai-files.state.json create mode 100644 convex/_generated/ai/guidelines.md create mode 100644 skills-lock.json diff --git a/.claude/skills/convex-create-component b/.claude/skills/convex-create-component new file mode 120000 index 0000000..dfa8244 --- /dev/null +++ b/.claude/skills/convex-create-component @@ -0,0 +1 @@ +../../.agents/skills/convex-create-component \ No newline at end of file diff --git a/.claude/skills/convex-migration-helper b/.claude/skills/convex-migration-helper new file mode 120000 index 0000000..81eeed1 --- /dev/null +++ b/.claude/skills/convex-migration-helper @@ -0,0 +1 @@ +../../.agents/skills/convex-migration-helper \ No newline at end of file diff --git a/.claude/skills/convex-performance-audit b/.claude/skills/convex-performance-audit new file mode 120000 index 0000000..1bff1e5 --- /dev/null +++ b/.claude/skills/convex-performance-audit @@ -0,0 +1 @@ +../../.agents/skills/convex-performance-audit \ No newline at end of file diff --git a/.claude/skills/convex-quickstart b/.claude/skills/convex-quickstart new file mode 120000 index 0000000..9edf197 --- /dev/null +++ b/.claude/skills/convex-quickstart @@ -0,0 +1 @@ +../../.agents/skills/convex-quickstart \ No newline at end of file diff --git a/.claude/skills/convex-setup-auth b/.claude/skills/convex-setup-auth new file mode 120000 index 0000000..a19c837 --- /dev/null +++ b/.claude/skills/convex-setup-auth @@ -0,0 +1 @@ +../../.agents/skills/convex-setup-auth \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ba21163 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ + +This project uses [Convex](https://convex.dev) as its backend. + +When working on Convex code, **always read `convex/_generated/ai/guidelines.md` first** for important guidelines on how to correctly use Convex APIs and patterns. The file contains rules that override what you may have learned about Convex from training data. + +Convex agent skills for common tasks can be installed by running `npx convex ai-files install`. + diff --git a/convex/_generated/ai/ai-files.state.json b/convex/_generated/ai/ai-files.state.json new file mode 100644 index 0000000..eb4c8eb --- /dev/null +++ b/convex/_generated/ai/ai-files.state.json @@ -0,0 +1,13 @@ +{ + "guidelinesHash": "deca8f973b701e27f65ab9d2b189201fcbcefcf524e9bc44c413ab3109e35993", + "agentsMdSectionHash": "bbf30bd25ceea0aefd279d62e1cb2b4c207fcb712b69adf26f3d02b296ffc7b2", + "claudeMdHash": "bbf30bd25ceea0aefd279d62e1cb2b4c207fcb712b69adf26f3d02b296ffc7b2", + "agentSkillsSha": "cd697e02ca46ccbc445418785e03ad87a35617c9", + "installedSkillNames": [ + "convex-create-component", + "convex-migration-helper", + "convex-performance-audit", + "convex-quickstart", + "convex-setup-auth" + ] +} diff --git a/convex/_generated/ai/guidelines.md b/convex/_generated/ai/guidelines.md new file mode 100644 index 0000000..5b87255 --- /dev/null +++ b/convex/_generated/ai/guidelines.md @@ -0,0 +1,303 @@ +# Convex guidelines +## Function guidelines +### Http endpoint syntax +- HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator. For example: +```typescript +import { httpRouter } from "convex/server"; +import { httpAction } from "./_generated/server"; +const http = httpRouter(); +http.route({ + path: "/echo", + method: "POST", + handler: httpAction(async (ctx, req) => { + const body = await req.bytes(); + return new Response(body, { status: 200 }); + }), +}); +``` +- HTTP endpoints are always registered at the exact path you specify in the `path` field. For example, if you specify `/api/someRoute`, the endpoint will be registered at `/api/someRoute`. + +### Validators +- Below is an example of an array validator: +```typescript +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ +args: { + simpleArray: v.array(v.union(v.string(), v.number())), +}, +handler: async (ctx, args) => { + //... +}, +}); +``` +- Below is an example of a schema with validators that codify a discriminated union type: +```typescript +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + results: defineTable( + v.union( + v.object({ + kind: v.literal("error"), + errorMessage: v.string(), + }), + v.object({ + kind: v.literal("success"), + value: v.number(), + }), + ), + ) +}); +``` +- Here are the valid Convex types along with their respective validators: +Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes | +| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Id | string | `doc._id` | `v.id(tableName)` | | +| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. | +| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. | +| Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. | +| Boolean | boolean | `true` | `v.boolean()` | +| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. | +| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. | +| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. | +| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". | +| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "_". | + +### Function registration +- Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`. +- Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private. +- You CANNOT register a function through the `api` or `internal` objects. +- ALWAYS include argument validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. + +### Function calling +- Use `ctx.runQuery` to call a query from a query, mutation, or action. +- Use `ctx.runMutation` to call a mutation from a mutation or action. +- Use `ctx.runAction` to call an action from an action. +- ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead. +- Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions. +- All of these calls take in a `FunctionReference`. Do NOT try to pass the callee function directly into one of these calls. +- When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example, +``` +export const f = query({ + args: { name: v.string() }, + handler: async (ctx, args) => { + return "Hello " + args.name; + }, +}); + +export const g = query({ + args: {}, + handler: async (ctx, args) => { + const result: string = await ctx.runQuery(api.example.f, { name: "Bob" }); + return null; + }, +}); +``` + +### Function references +- Use the `api` object defined by the framework in `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`. +- Use the `internal` object defined by the framework in `convex/_generated/api.ts` to call internal (or private) functions registered with `internalQuery`, `internalMutation`, or `internalAction`. +- Convex uses file-based routing, so a public function defined in `convex/example.ts` named `f` has a function reference of `api.example.f`. +- A private function defined in `convex/example.ts` named `g` has a function reference of `internal.example.g`. +- Functions can also registered within directories nested within the `convex/` folder. For example, a public function `h` defined in `convex/messages/access.ts` has a function reference of `api.messages.access.h`. + +### Pagination +- Define pagination using the following syntax: + +```ts +import { v } from "convex/values"; +import { query, mutation } from "./_generated/server"; +import { paginationOptsValidator } from "convex/server"; +export const listWithExtraArg = query({ + args: { paginationOpts: paginationOptsValidator, author: v.string() }, + handler: async (ctx, args) => { + return await ctx.db + .query("messages") + .withIndex("by_author", (q) => q.eq("author", args.author)) + .order("desc") + .paginate(args.paginationOpts); + }, +}); +``` +Note: `paginationOpts` is an object with the following properties: +- `numItems`: the maximum number of documents to return (the validator is `v.number()`) +- `cursor`: the cursor to use to fetch the next page of documents (the validator is `v.union(v.string(), v.null())`) +- A query that ends in `.paginate()` returns an object that has the following properties: +- page (contains an array of documents that you fetches) +- isDone (a boolean that represents whether or not this is the last page of documents) +- continueCursor (a string that represents the cursor to use to fetch the next page of documents) + + +## Schema guidelines +- Always define your schema in `convex/schema.ts`. +- Always import the schema definition functions from `convex/server`. +- System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`. +- Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2". +- Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes. +- Do not store unbounded lists as an array field inside a document (e.g. `v.array(v.object({...}))`). As the array grows it will hit the 1MB document size limit, and every update rewrites the entire document. Instead, create a separate table for the child items with a foreign key back to the parent. + +## Authentication guidelines +- Convex supports JWT-based authentication through `convex/auth.config.ts`. ALWAYS create this file when using authentication. Without it, `ctx.auth.getUserIdentity()` will always return `null`. +- Example `convex/auth.config.ts`: +```typescript +export default { + providers: [ + { + domain: "https://your-auth-provider.com", + applicationID: "convex", + }, + ], +}; +``` +The `domain` must be the issuer URL of the JWT provider. Convex fetches `{domain}/.well-known/openid-configuration` to discover the JWKS endpoint. The `applicationID` is checked against the JWT `aud` (audience) claim. +- Use `ctx.auth.getUserIdentity()` to get the authenticated user's identity in any query, mutation, or action. This returns `null` if the user is not authenticated, or a `UserIdentity` object with fields like `subject`, `issuer`, `name`, `email`, etc. The `subject` field is the unique user identifier. +- In Convex `UserIdentity`, `tokenIdentifier` is guaranteed and is the canonical stable identifier for the authenticated identity. For any auth-linked database lookup or ownership check, prefer `identity.tokenIdentifier` over `identity.subject`. Do NOT use `identity.subject` alone as a global identity key. +- NEVER accept a `userId` or any user identifier as a function argument for authorization purposes. Always derive the user identity server-side via `ctx.auth.getUserIdentity()`. +- When using an external auth provider with Convex on the client, use `ConvexProviderWithAuth` instead of `ConvexProvider`: +```tsx +import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react"; + +const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + +function App({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` +The `useAuth` prop must return `{ isLoading, isAuthenticated, fetchAccessToken }`. Do NOT use plain `ConvexProvider` when authentication is needed — it will not send tokens with requests. + +## Typescript guidelines +- You can use the helper typescript type `Id` imported from './_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use `Id<'users'>` to get the type of the id for that table. +- Use `Doc<"tableName">` from `./_generated/dataModel` to get the full document type for a table. +- Use `QueryCtx`, `MutationCtx`, `ActionCtx` from `./_generated/server` for typing function contexts. NEVER use `any` for ctx parameters — always use the proper context type. +- If you need to define a `Record` make sure that you correctly provide the type of the key and value in the type. For example a validator `v.record(v.id('users'), v.string())` would have the type `Record, string>`. Below is an example of using `Record` with an `Id` type in a query: +```ts +import { query } from "./_generated/server"; +import { Doc, Id } from "./_generated/dataModel"; + +export const exampleQuery = query({ + args: { userIds: v.array(v.id("users")) }, + handler: async (ctx, args) => { + const idToUsername: Record, string> = {}; + for (const userId of args.userIds) { + const user = await ctx.db.get("users", userId); + if (user) { + idToUsername[user._id] = user.username; + } + } + + return idToUsername; + }, +}); +``` +- Be strict with types, particularly around id's of documents. For example, if a function takes in an id for a document in the 'users' table, take in `Id<'users'>` rather than `string`. + +## Full text search guidelines +- A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like: + +const messages = await ctx.db + .query("messages") + .withSearchIndex("search_body", (q) => + q.search("body", "hello hi").eq("channel", "#general"), + ) + .take(10); + +## Query guidelines +- Do NOT use `filter` in queries. Instead, define an index in the schema and use `withIndex` instead. +- If the user does not explicitly tell you to return all results from a query you should ALWAYS return a bounded collection instead. So that is instead of using `.collect()` you should use `.take()` or paginate on database queries. This prevents future performance issues when tables grow in an unbounded way. +- Never use `.collect().length` to count rows. Convex has no built-in count operator, so if you need a count that stays efficient at scale, maintain a denormalized counter in a separate document and update it in your mutations. +- Convex queries do NOT support `.delete()`. If you need to delete all documents matching a query, use `.take(n)` to read them in batches, iterate over each batch calling `ctx.db.delete(row._id)`, and repeat until no more results are returned. +- Convex mutations are transactions with limits on the number of documents read and written. If a mutation needs to process more documents than fit in a single transaction (e.g. bulk deletion on a large table), process a batch with `.take(n)` and then call `ctx.scheduler.runAfter(0, api.myModule.myMutation, args)` to schedule itself to continue. This way each invocation stays within transaction limits. +- Use `.unique()` to get a single document from a query. This method will throw an error if there are multiple documents that match the query. +- When using async iteration, don't use `.collect()` or `.take(n)` on the result of a query. Instead, use the `for await (const row of query)` syntax. +### Ordering +- By default Convex always returns documents in ascending `_creationTime` order. +- You can use `.order('asc')` or `.order('desc')` to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending. +- Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans. + + +## Mutation guidelines +- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })` +- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.patch('tasks', taskId, { completed: true })` + +## Action guidelines +- Always add `"use node";` to the top of files containing actions that use Node.js built-in modules. +- Never add `"use node";` to a file that also exports queries or mutations. Only actions can run in the Node.js runtime; queries and mutations must stay in the default Convex runtime. If you need Node.js built-ins alongside queries or mutations, put the action in a separate file. +- `fetch()` is available in the default Convex runtime. You do NOT need `"use node";` just to use `fetch()`. +- Never use `ctx.db` inside of an action. Actions don't have access to the database. +- Below is an example of the syntax for an action: +```ts +import { action } from "./_generated/server"; + +export const exampleAction = action({ + args: {}, + handler: async (ctx, args) => { + console.log("This action does not return anything"); + return null; + }, +}); +``` + +## Scheduling guidelines +### Cron guidelines +- Only use the `crons.interval` or `crons.cron` methods to schedule cron jobs. Do NOT use the `crons.hourly`, `crons.daily`, or `crons.weekly` helpers. +- Both cron methods take in a FunctionReference. Do NOT try to pass the function directly into one of these methods. +- Define crons by declaring the top-level `crons` object, calling some methods on it, and then exporting it as default. For example, +```ts +import { cronJobs } from "convex/server"; +import { internal } from "./_generated/api"; +import { internalAction } from "./_generated/server"; + +const empty = internalAction({ + args: {}, + handler: async (ctx, args) => { + console.log("empty"); + }, +}); + +const crons = cronJobs(); + +// Run `internal.crons.empty` every two hours. +crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {}); + +export default crons; +``` +- You can register Convex functions within `crons.ts` just like any other file. +- If a cron calls an internal function, always import the `internal` object from '_generated/api', even if the internal function is registered in the same file. + + +## File storage guidelines +- The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns `null` if the file doesn't exist. +- Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's metadata. + +Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`. +``` +import { query } from "./_generated/server"; +import { Id } from "./_generated/dataModel"; + +type FileMetadata = { + _id: Id<"_storage">; + _creationTime: number; + contentType?: string; + sha256: string; + size: number; +} + +export const exampleQuery = query({ + args: { fileId: v.id("_storage") }, + handler: async (ctx, args) => { + const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId); + console.log(metadata); + return null; + }, +}); +``` +- Convex storage stores items as `Blob` objects. You must convert all items to/from a `Blob` when using Convex storage. + + diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..84209c7 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "skills": { + "convex-create-component": { + "source": "get-convex/agent-skills", + "sourceType": "github", + "computedHash": "7d83a8297fceca8dca482c098e9062121d622d864e54e5ceb5397a2ee0f2c182" + }, + "convex-migration-helper": { + "source": "get-convex/agent-skills", + "sourceType": "github", + "computedHash": "3ded3c03c6693f07d9a79bda1c845b83dd1eabdd6c01e3dd35ecfebcfc6c1b04" + }, + "convex-performance-audit": { + "source": "get-convex/agent-skills", + "sourceType": "github", + "computedHash": "a9a2e552adece879b9a7f724e66dad6a62d8e63fab52973403aebfd55c0ed717" + }, + "convex-quickstart": { + "source": "get-convex/agent-skills", + "sourceType": "github", + "computedHash": "2dedd52d19c021b90cc7cd610c88e9a59d74c5c54667d4858e2da66349daf1ae" + }, + "convex-setup-auth": { + "source": "get-convex/agent-skills", + "sourceType": "github", + "computedHash": "8c76dd0ff0391aa5fd082249b6e064c6101e961225db7996e6f805683155dfd7" + } + } +} From 45c44d2c68266daa367ce9e0cc2b1cd7b6fa990a Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Fri, 20 Mar 2026 10:44:30 +0100 Subject: [PATCH 2/5] ai: Added expo skills --- .agents/skills/building-native-ui/SKILL.md | 321 ++++++++++ .../references/animations.md | 220 +++++++ .../building-native-ui/references/controls.md | 270 ++++++++ .../references/form-sheet.md | 253 ++++++++ .../references/gradients.md | 106 +++ .../building-native-ui/references/icons.md | 213 ++++++ .../building-native-ui/references/media.md | 198 ++++++ .../references/route-structure.md | 229 +++++++ .../building-native-ui/references/search.md | 248 +++++++ .../building-native-ui/references/storage.md | 121 ++++ .../building-native-ui/references/tabs.md | 433 +++++++++++++ .../references/toolbar-and-headers.md | 284 ++++++++ .../references/visual-effects.md | 197 ++++++ .../references/webgpu-three.md | 605 ++++++++++++++++++ .../references/zoom-transitions.md | 158 +++++ .../skills/convex-create-component/SKILL.md | 411 ++++++++++++ .../agents/openai.yaml | 10 + .../convex-create-component/assets/icon.svg | 3 + .../references/hybrid-components.md | 37 ++ .../references/local-components.md | 38 ++ .../references/packaged-components.md | 51 ++ .../skills/convex-migration-helper/SKILL.md | 524 +++++++++++++++ .../agents/openai.yaml | 10 + .../convex-migration-helper/assets/icon.svg | 3 + .../skills/convex-performance-audit/SKILL.md | 143 +++++ .../agents/openai.yaml | 10 + .../convex-performance-audit/assets/icon.svg | 3 + .../references/function-budget.md | 232 +++++++ .../references/hot-path-rules.md | 359 +++++++++++ .../references/occ-conflicts.md | 126 ++++ .../references/subscription-cost.md | 252 ++++++++ .agents/skills/convex-quickstart/SKILL.md | 337 ++++++++++ .../convex-quickstart/agents/openai.yaml | 10 + .../skills/convex-quickstart/assets/icon.svg | 4 + .agents/skills/convex-setup-auth/SKILL.md | 113 ++++ .../convex-setup-auth/agents/openai.yaml | 10 + .../skills/convex-setup-auth/assets/icon.svg | 3 + .../convex-setup-auth/references/auth0.md | 116 ++++ .../convex-setup-auth/references/clerk.md | 113 ++++ .../references/convex-auth.md | 143 +++++ .../references/workos-authkit.md | 114 ++++ .agents/skills/expo-cicd-workflows/SKILL.md | 92 +++ .../expo-cicd-workflows/scripts/fetch.js | 109 ++++ .../expo-cicd-workflows/scripts/package.json | 11 + .../expo-cicd-workflows/scripts/validate.js | 84 +++ .agents/skills/expo-deployment/SKILL.md | 190 ++++++ .../references/app-store-metadata.md | 479 ++++++++++++++ .../references/ios-app-store.md | 355 ++++++++++ .../expo-deployment/references/play-store.md | 246 +++++++ .../expo-deployment/references/testflight.md | 58 ++ .../expo-deployment/references/workflows.md | 200 ++++++ .agents/skills/expo-dev-client/SKILL.md | 164 +++++ .agents/skills/find-skills/SKILL.md | 142 ++++ .agents/skills/native-data-fetching/SKILL.md | 507 +++++++++++++++ .../references/expo-router-loaders.md | 341 ++++++++++ .agents/skills/upgrading-expo/SKILL.md | 133 ++++ .../references/expo-av-to-audio.md | 132 ++++ .../references/expo-av-to-video.md | 160 +++++ .../upgrading-expo/references/native-tabs.md | 124 ++++ .../references/new-architecture.md | 79 +++ .../upgrading-expo/references/react-19.md | 79 +++ .../references/react-compiler.md | 59 ++ .claude/skills/building-native-ui | 1 + .claude/skills/expo-cicd-workflows | 1 + .claude/skills/expo-deployment | 1 + .claude/skills/expo-dev-client | 1 + .claude/skills/find-skills | 1 + .claude/skills/native-data-fetching | 1 + .claude/skills/upgrading-expo | 1 + CLAUDE.md | 9 + skills-lock.json | 35 + 71 files changed, 10826 insertions(+) create mode 100644 .agents/skills/building-native-ui/SKILL.md create mode 100644 .agents/skills/building-native-ui/references/animations.md create mode 100644 .agents/skills/building-native-ui/references/controls.md create mode 100644 .agents/skills/building-native-ui/references/form-sheet.md create mode 100644 .agents/skills/building-native-ui/references/gradients.md create mode 100644 .agents/skills/building-native-ui/references/icons.md create mode 100644 .agents/skills/building-native-ui/references/media.md create mode 100644 .agents/skills/building-native-ui/references/route-structure.md create mode 100644 .agents/skills/building-native-ui/references/search.md create mode 100644 .agents/skills/building-native-ui/references/storage.md create mode 100644 .agents/skills/building-native-ui/references/tabs.md create mode 100644 .agents/skills/building-native-ui/references/toolbar-and-headers.md create mode 100644 .agents/skills/building-native-ui/references/visual-effects.md create mode 100644 .agents/skills/building-native-ui/references/webgpu-three.md create mode 100644 .agents/skills/building-native-ui/references/zoom-transitions.md create mode 100644 .agents/skills/convex-create-component/SKILL.md create mode 100644 .agents/skills/convex-create-component/agents/openai.yaml create mode 100644 .agents/skills/convex-create-component/assets/icon.svg create mode 100644 .agents/skills/convex-create-component/references/hybrid-components.md create mode 100644 .agents/skills/convex-create-component/references/local-components.md create mode 100644 .agents/skills/convex-create-component/references/packaged-components.md create mode 100644 .agents/skills/convex-migration-helper/SKILL.md create mode 100644 .agents/skills/convex-migration-helper/agents/openai.yaml create mode 100644 .agents/skills/convex-migration-helper/assets/icon.svg create mode 100644 .agents/skills/convex-performance-audit/SKILL.md create mode 100644 .agents/skills/convex-performance-audit/agents/openai.yaml create mode 100644 .agents/skills/convex-performance-audit/assets/icon.svg create mode 100644 .agents/skills/convex-performance-audit/references/function-budget.md create mode 100644 .agents/skills/convex-performance-audit/references/hot-path-rules.md create mode 100644 .agents/skills/convex-performance-audit/references/occ-conflicts.md create mode 100644 .agents/skills/convex-performance-audit/references/subscription-cost.md create mode 100644 .agents/skills/convex-quickstart/SKILL.md create mode 100644 .agents/skills/convex-quickstart/agents/openai.yaml create mode 100644 .agents/skills/convex-quickstart/assets/icon.svg create mode 100644 .agents/skills/convex-setup-auth/SKILL.md create mode 100644 .agents/skills/convex-setup-auth/agents/openai.yaml create mode 100644 .agents/skills/convex-setup-auth/assets/icon.svg create mode 100644 .agents/skills/convex-setup-auth/references/auth0.md create mode 100644 .agents/skills/convex-setup-auth/references/clerk.md create mode 100644 .agents/skills/convex-setup-auth/references/convex-auth.md create mode 100644 .agents/skills/convex-setup-auth/references/workos-authkit.md create mode 100644 .agents/skills/expo-cicd-workflows/SKILL.md create mode 100644 .agents/skills/expo-cicd-workflows/scripts/fetch.js create mode 100644 .agents/skills/expo-cicd-workflows/scripts/package.json create mode 100644 .agents/skills/expo-cicd-workflows/scripts/validate.js create mode 100644 .agents/skills/expo-deployment/SKILL.md create mode 100644 .agents/skills/expo-deployment/references/app-store-metadata.md create mode 100644 .agents/skills/expo-deployment/references/ios-app-store.md create mode 100644 .agents/skills/expo-deployment/references/play-store.md create mode 100644 .agents/skills/expo-deployment/references/testflight.md create mode 100644 .agents/skills/expo-deployment/references/workflows.md create mode 100644 .agents/skills/expo-dev-client/SKILL.md create mode 100644 .agents/skills/find-skills/SKILL.md create mode 100644 .agents/skills/native-data-fetching/SKILL.md create mode 100644 .agents/skills/native-data-fetching/references/expo-router-loaders.md create mode 100644 .agents/skills/upgrading-expo/SKILL.md create mode 100644 .agents/skills/upgrading-expo/references/expo-av-to-audio.md create mode 100644 .agents/skills/upgrading-expo/references/expo-av-to-video.md create mode 100644 .agents/skills/upgrading-expo/references/native-tabs.md create mode 100644 .agents/skills/upgrading-expo/references/new-architecture.md create mode 100644 .agents/skills/upgrading-expo/references/react-19.md create mode 100644 .agents/skills/upgrading-expo/references/react-compiler.md create mode 120000 .claude/skills/building-native-ui create mode 120000 .claude/skills/expo-cicd-workflows create mode 120000 .claude/skills/expo-deployment create mode 120000 .claude/skills/expo-dev-client create mode 120000 .claude/skills/find-skills create mode 120000 .claude/skills/native-data-fetching create mode 120000 .claude/skills/upgrading-expo 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