diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 00000000..0e140b44 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,106 @@ +# Expo Examples — Integration Skills + +Add popular integrations to an Expo project using curated skills from this repository. + +## How it works + +Each `with-*` example folder contains a `SKILL.md` with complete implementation instructions. Use the table below to find the right skill, then fetch and follow it. + +## Available integrations + +| Category | Options | Skill | +|----------|---------|-------| +| **Authentication** | Clerk, Auth0, Better Auth | See [Authentication](#authentication) | +| **Payments** | Stripe (native + web) | `with-stripe` | +| **Database** | SQLite, LibSQL (Turso), Convex | See [Database](#database) | +| **Styling** | TailwindCSS (Nativewind) | `with-tailwindcss` | +| **API Client** | Apollo GraphQL, GraphQL (Yoga + URQL) | See [API Client](#api-client) | +| **Animations** | Reanimated, Moti | See [Animations](#animations) | +| **State Management** | Zustand | `with-zustand` | +| **Device Features** | Camera, Maps | See [Device Features](#device-features) | + +## Fetching a skill + +Once you know the skill name (e.g. `with-clerk`), fetch its SKILL.md: + +``` +https://raw.githubusercontent.com/expo/examples/master/with-clerk/SKILL.md +``` + +Then follow the instructions in the fetched SKILL.md to implement the integration. + +--- + +## Category decision guides + +Use these when the user hasn't specified a particular library. + +### Authentication + +| Option | Best for | Trade-offs | +|--------|----------|------------| +| **Clerk** (recommended) | Quick setup, managed auth with pre-built UI, social login, MFA | Requires Clerk account, usage-based pricing | +| **Auth0** | Enterprise SSO, SAML, compliance requirements | More complex setup, requires Auth0 account | +| **Better Auth** | Full control, self-hosted, open-source with Prisma | More setup (Prisma, database), self-hosted responsibility | + +**Quick decision:** Need it fast? → `with-clerk`. Enterprise SSO? → `with-auth0`. Self-hosted control? → `with-better-auth`. Not sure? → `with-clerk`. + +### Database + +| Option | Best for | Trade-offs | +|--------|----------|------------| +| **SQLite** (recommended) | Local offline-first storage, no server needed | No cloud sync, data stays on device | +| **LibSQL** (Turso) | Local-first with cloud sync | Requires Turso account, sync management | +| **Convex** | Real-time data, collaborative features, full backend | Requires internet, Convex account, vendor lock-in | + +**Quick decision:** Offline-only? → `with-sqlite`. Need sync? → `with-libsql`. Real-time? → `with-convex`. Not sure? → `with-sqlite`. + +### API Client + +| Option | Best for | Trade-offs | +|--------|----------|------------| +| **Apollo Client** | Connecting to an existing external GraphQL API | Client-only, no server included | +| **GraphQL Full-Stack** (Yoga + URQL + gql.tada) | Building a GraphQL API from scratch within the Expo app | More complex, requires server output mode | + +**Quick decision:** Have an API? → `with-apollo`. Building one? → `with-graphql`. + +### Animations + +| Option | Best for | Trade-offs | +|--------|----------|------------| +| **Reanimated** (recommended) | Complex, gesture-driven, performance-critical animations | More verbose API | +| **Moti** | Simple declarative animations, enter/exit transitions, skeleton loading | Less control, adds dependency (requires Reanimated) | + +**Quick decision:** Complex/gesture animations? → `with-reanimated`. Simple fade/scale/mount? → `with-moti`. Not sure? → `with-reanimated`. + +### Device Features + +| Option | What it does | +|--------|-------------| +| **Camera** | Photo capture, video recording, front/back switching via `expo-camera` | +| **Maps** | Interactive maps with markers and overlays via `react-native-maps` | + +**Quick decision:** Photos/video? → `with-camera`. Location display? → `with-maps`. + +--- + +## Adaptation rules + +These apply to ALL integrations: + +- **Merge dependencies** — add to existing `package.json`, never replace it +- **Merge plugins** — add to existing `app.json` plugins array +- **Adapt navigation** — use the project's existing pattern (Expo Router / React Navigation) +- **Match styling** — follow the project's existing styling approach +- **Create, don't overwrite** — add new files, don't replace existing ones +- **Preserve structure** — follow the project's existing directory conventions + +## Discovery + +To discover additional integration skills beyond those listed above, browse: + +``` +https://github.com/expo/examples +``` + +Any `with-*` folder containing a `SKILL.md` is a fetchable integration skill. diff --git a/with-apollo/SKILL.md b/with-apollo/SKILL.md new file mode 100644 index 00000000..7302558f --- /dev/null +++ b/with-apollo/SKILL.md @@ -0,0 +1,167 @@ +--- +name: with-apollo +description: Add Apollo Client for GraphQL to an Expo project. Connects to external GraphQL APIs with caching and state management. Use when the user wants Apollo, GraphQL client, or needs to query an external GraphQL endpoint. +version: 1.0.0 +license: MIT +--- + +# Add Apollo GraphQL Client + +## When to use + +- User wants to connect to an external GraphQL API +- User prefers Apollo Client over URQL +- User needs GraphQL caching and state management + +## Dependencies + +```bash +npm install @apollo/client graphql +``` + +Optional for authenticated requests: +```bash +npm install @apollo/link-context +``` + +## Implementation + +### 1. Create Apollo client + +Create `utils/apollo.js` (or `.ts`): + +```tsx +import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; + +const client = new ApolloClient({ + link: new HttpLink({ + uri: "https://your-graphql-endpoint.com/graphql", + }), + cache: new InMemoryCache(), +}); + +export default client; +``` + +### 2. (Optional) Add authentication + +```tsx +import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client"; +import { setContext } from "@apollo/link-context"; + +const httpLink = new HttpLink({ + uri: "https://your-graphql-endpoint.com/graphql", +}); + +const authLink = setContext(async (_, { headers }) => { + const token = await getToken(); // your auth token logic + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; +}); + +const client = new ApolloClient({ + link: from([authLink, httpLink]), + cache: new InMemoryCache(), +}); + +export default client; +``` + +### 3. Wrap app with ApolloProvider + +In root layout or App component: + +```tsx +import { ApolloProvider } from "@apollo/client"; +import client from "@/utils/apollo"; + +export default function RootLayout() { + return ( + + {/* rest of the app */} + + ); +} +``` + +### 4. Define and use queries + +```tsx +import { gql, useQuery } from "@apollo/client"; + +const GET_ITEMS = gql` + query GetItems { + items { + id + name + } + } +`; + +export default function ItemsScreen() { + const { loading, error, data } = useQuery(GET_ITEMS); + + if (loading) return Loading...; + if (error) return Error: {error.message}; + + return ( + {item.name}} + /> + ); +} +``` + +### 5. Mutations + +```tsx +import { gql, useMutation } from "@apollo/client"; + +const CREATE_ITEM = gql` + mutation CreateItem($name: String!) { + createItem(name: $name) { + id + name + } + } +`; + +function CreateItemButton() { + const [createItem, { loading }] = useMutation(CREATE_ITEM, { + refetchQueries: [GET_ITEMS], + }); + + return ( + ; +} + +type Props = { dom?: import("expo/dom").DOMProps }; +``` + +Use from a native route: + +```tsx +import MyDomComponent from "@/components/dom/my-component"; + +export default function MyScreen() { + return console.log("pressed")} />; +} +``` + +### Platform-specific layouts + +Use `.web.tsx` suffix for web-specific layouts: +- `_layout.tsx` — native layout with tab bar +- `_layout.web.tsx` — web layout with shadcn sidebar navigation + +### Bridging native APIs from DOM Components + +DOM Components can call native functions via props: + +```tsx +// DOM component receives native callbacks as props +export default function Dashboard({ onHaptic }: { onHaptic: () => Promise } & Props) { + return ; +} + +// Native route passes haptics + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); +}} /> +``` + +## Key component reference + +| Component | Description | +|-----------|-------------| +| Button | Variant-based button (default, destructive, outline, secondary, ghost, link) | +| Card | Container with header, content, footer sections | +| Table | Data table with sorting and selection | +| Chart | Recharts wrapper with theme-aware colors | +| Sidebar | Collapsible navigation sidebar | +| Sheet/Drawer | Slide-out panels | +| Tabs | Tab navigation | +| Badge | Status indicator labels | + +## Adaptation notes + +- Merge dependencies — do not replace `package.json` +- The `"use dom"` directive is required for shadcn components on native — they render in a web view +- Import `@/global.css` at the top of each DOM component file and in the web root layout +- `rsc: false` in `components.json` is required since Expo does not use React Server Components +- shadcn components use standard HTML elements, not React Native primitives — they only work inside DOM components on native +- Use platform-specific layout files (`.web.tsx`) when navigation differs between native and web + +## Reference + +See full working example in this directory. diff --git a/with-skia/SKILL.md b/with-skia/SKILL.md new file mode 100644 index 00000000..5b9cbf32 --- /dev/null +++ b/with-skia/SKILL.md @@ -0,0 +1,160 @@ +--- +name: with-skia +description: Add Skia 2D graphics to an Expo project. GPU-accelerated drawing, shaders, and canvas rendering with React Native Skia. Use when the user wants Skia, custom shaders, 2D graphics, or canvas drawing. +version: 1.0.0 +license: MIT +--- + +# Add Skia 2D Graphics + +## When to use + +- User wants GPU-accelerated 2D graphics or canvas drawing +- User asks about Skia or React Native Skia +- User needs custom GLSL shaders or runtime effects +- User wants to render shapes, paths, images, or visual effects + +## Dependencies + +```bash +npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets +``` + +## Configuration + +No additional Expo config plugins are required. For web support, copy `canvaskit.wasm` to the `public/` folder. This project includes a `postinstall` script that handles it automatically: + +```json +{ + "scripts": { + "postinstall": "node copy-canvaskit.js" + } +} +``` + +On web, Skia must be loaded asynchronously before use. Use a platform-specific helper: + +**components/async-skia.tsx** (web): +```tsx +import { use } from "react"; +import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/module/web"; + +let skiaPromise: Promise | null = null; + +function loadSkia() { + if (!skiaPromise) skiaPromise = LoadSkiaWeb(); + return skiaPromise; +} + +export function AsyncSkia() { + use(loadSkia()); + return null; +} +``` + +**components/async-skia.native.tsx** (native): +```tsx +export function AsyncSkia() { + return null; +} +``` + +## Implementation + +### Canvas with a runtime shader + +```tsx +import React from "react"; +import { + Canvas, + Skia, + Shader, + Fill, + useClock, +} from "@shopify/react-native-skia"; +import { useDerivedValue } from "react-native-reanimated"; +import { useWindowDimensions } from "react-native"; + +const source = Skia.RuntimeEffect.Make(` +uniform vec3 uResolution; +uniform float uTime; + +vec4 main(vec2 fragCoord) { + vec2 uv = fragCoord / uResolution.xy; + float d = -uTime * 0.5; + float a = 0.0; + for (float i = 0.0; i < 8.0; ++i) { + a += cos(i - d - a * uv.x); + d += sin(uv.y * i + a); + } + d += uTime * 0.5; + vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5); + return vec4(col, 1.0); +} +`); + +export default function ShaderExample() { + const clock = useClock(); + const { width, height } = useWindowDimensions(); + + const uniforms = useDerivedValue(() => ({ + uResolution: [width, height, width / height], + uTime: clock.value / 1000, + }), [clock, width, height]); + + return ( + + + + + + ); +} +``` + +### Using Skia inside a screen with Suspense + +```tsx +import React from "react"; +import { ActivityIndicator, View } from "react-native"; +import { AsyncSkia } from "../components/async-skia"; + +const ShaderExample = React.lazy(() => import("../components/shader-example")); + +export default function Page() { + return ( + + }> + + + + + ); +} +``` + +## Key API reference + +| API | Purpose | +|-----|---------| +| `Canvas` | Root drawing surface for Skia content | +| `Fill` | Fills the canvas with a paint or shader | +| `Shader` | Applies a runtime shader effect | +| `Skia.RuntimeEffect.Make(glsl)` | Compiles a GLSL shader string | +| `useClock()` | Shared value that ticks every frame (ms) | +| `useDerivedValue(() => value)` | Derives animated uniform values (from Reanimated) | +| `LoadSkiaWeb()` | Loads the CanvasKit WASM module on web | + +## Adaptation notes + +- Merge dependencies — do not replace `package.json` +- `react-native-reanimated` and `react-native-worklets` are peer dependencies required by Skia +- On web, always wrap Skia components in `React.Suspense` with `AsyncSkia` to load CanvasKit first +- On native, Skia is bundled in the binary and no async loading is needed +- Use the platform-specific `.native.tsx` / `.tsx` file convention for the async loader +- The `postinstall` script must run after `npm install` to copy `canvaskit.wasm` into `public/` +- Shader uniforms are passed as plain objects; use `useDerivedValue` from Reanimated to animate them + +## Reference + +See full working example in this directory. diff --git a/with-socket-io/SKILL.md b/with-socket-io/SKILL.md new file mode 100644 index 00000000..c0861fd3 --- /dev/null +++ b/with-socket-io/SKILL.md @@ -0,0 +1,143 @@ +--- +name: with-socket-io +description: Add Socket.IO real-time communication to an Expo project. WebSocket-based bidirectional messaging between client and server. Use when the user wants Socket.IO, real-time updates, WebSockets, live data, or push messages from a server. +version: 1.0.0 +license: MIT +--- + +# Add Socket.IO Real-Time Communication + +## When to use + +- User wants real-time bidirectional communication between client and server +- User asks about Socket.IO or WebSockets in Expo +- User wants live data pushed from a server (e.g. notifications, chat, live updates) + +## Dependencies + +Client (Expo app): + +```bash +npm install socket.io-client +``` + +Backend (Node.js server): + +```bash +npm install express socket.io +``` + +## Configuration + +Set the `socketEndpoint` to point at the running backend: + +```tsx +const socketEndpoint = "http://localhost:3000"; +``` + +On a physical device, replace `localhost` with your machine's LAN IP address. + +## Implementation + +### 1. Create the backend server + +Create `backend/index.js`: + +```js +const app = require("express")(); +const http = require("http").Server(app); +const io = require("socket.io")(http); + +io.on("connection", (socket) => { + console.log(`[${socket.id}] socket connected`); + socket.on("disconnect", (reason) => { + console.log(`[${socket.id}] socket disconnected - ${reason}`); + }); +}); + +// Broadcast the current server time every 1 second +setInterval(() => { + io.sockets.emit("time-msg", { time: new Date().toISOString() }); +}, 1000); + +http.listen(3000, () => { + console.log("listening on *:3000"); +}); +``` + +Start it with `node backend/index.js`. + +### 2. Connect from the Expo app + +```tsx +import { useEffect, useState } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import io from "socket.io-client"; + +const socketEndpoint = "http://localhost:3000"; + +export default function App() { + const [hasConnection, setConnection] = useState(false); + const [time, setTime] = useState(null); + + useEffect(function didMount() { + const socket = io(socketEndpoint, { + transports: ["websocket"], + }); + + socket.io.on("open", () => setConnection(true)); + socket.io.on("close", () => setConnection(false)); + + socket.on("time-msg", (data) => { + setTime(new Date(data.time).toString()); + }); + + return function didUnmount() { + socket.disconnect(); + socket.removeAllListeners(); + }; + }, []); + + return ( + + {!hasConnection && ( + Connecting to {socketEndpoint}... + )} + {hasConnection && ( + <> + Server time + {time} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, justifyContent: "center", alignItems: "center" }, +}); +``` + +## Key API reference + +| API | Purpose | +|-----|---------| +| `io(url, opts)` | Create a socket connection; use `transports: ["websocket"]` for React Native | +| `socket.on(event, callback)` | Listen for a named event from the server | +| `socket.emit(event, data)` | Send a named event to the server | +| `socket.io.on("open" / "close", cb)` | Monitor connection state | +| `socket.disconnect()` | Close the connection | +| `io.sockets.emit(event, data)` | Server: broadcast to all connected clients | + +## Adaptation notes + +- Merge dependencies — don't replace `package.json` +- Use `transports: ["websocket"]` to avoid long-polling issues in React Native +- Replace `localhost` with a LAN IP or public URL when testing on physical devices +- Clean up the socket connection in the `useEffect` return to avoid memory leaks +- Adapt event names (`time-msg`) and data shapes to the user's actual use case +- The backend is a minimal Express + Socket.IO server; adapt or replace with the user's own server + +## Reference + +See full working example in this directory. diff --git a/with-sqlite/SKILL.md b/with-sqlite/SKILL.md new file mode 100644 index 00000000..d0d498e2 --- /dev/null +++ b/with-sqlite/SKILL.md @@ -0,0 +1,182 @@ +--- +name: with-sqlite +description: Add SQLite local database to an Expo project. Provides offline-first data storage with migrations, CRUD operations, and transactions. Use when the user wants a local database, offline storage, SQLite, or persistent data. +version: 1.0.0 +license: MIT +--- + +# Add SQLite Database + +## When to use + +- User wants local/offline data storage +- User asks about SQLite, local database, or persistent storage +- User needs CRUD operations with structured data + +## Dependencies + +```bash +npx expo install expo-sqlite +``` + +## Configuration + +### app.json + +Add the plugin (if not already present): + +```json +{ + "expo": { + "plugins": ["expo-sqlite"] + } +} +``` + +## Implementation + +### 1. Create database migration + +Create `utils/database.ts`: + +```tsx +import type { SQLiteDatabase } from "expo-sqlite"; + +export async function migrateDbIfNeeded(db: SQLiteDatabase) { + const DATABASE_VERSION = 1; + + const result = await db.getFirstAsync<{ user_version: number }>( + "PRAGMA user_version" + ); + let currentVersion = result?.user_version ?? 0; + + if (currentVersion >= DATABASE_VERSION) return; + + if (currentVersion === 0) { + await db.execAsync(` + PRAGMA journal_mode = 'wal'; + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY NOT NULL, + value TEXT NOT NULL, + done INTEGER NOT NULL DEFAULT 0 + ); + `); + currentVersion = 1; + } + + // Add future migrations here: + // if (currentVersion === 1) { ... currentVersion = 2; } + + await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); +} +``` + +Adapt the schema to the user's data model. + +### 2. Wrap app with SQLiteProvider + +In the root layout or a parent component: + +```tsx +import { SQLiteProvider } from "expo-sqlite"; +import { migrateDbIfNeeded } from "@/utils/database"; + +export default function RootLayout() { + return ( + + {/* rest of the app */} + + ); +} +``` + +### 3. Use the database in components + +```tsx +import { useSQLiteContext } from "expo-sqlite"; + +export function ItemList() { + const db = useSQLiteContext(); + const [items, setItems] = useState([]); + + // Read + const fetchItems = async () => { + const result = await db.getAllAsync("SELECT * FROM items"); + setItems(result); + }; + + // Create + const addItem = async (value: string) => { + await db.runAsync("INSERT INTO items (value, done) VALUES (?, 0)", value); + await fetchItems(); + }; + + // Update + const toggleItem = async (id: number, done: boolean) => { + await db.runAsync("UPDATE items SET done = ? WHERE id = ?", done ? 1 : 0, id); + await fetchItems(); + }; + + // Delete + const deleteItem = async (id: number) => { + await db.runAsync("DELETE FROM items WHERE id = ?", id); + await fetchItems(); + }; + + useEffect(() => { fetchItems(); }, []); + + return (/* render items */); +} +``` + +### 4. Use transactions for atomic operations + +```tsx +const fetchItems = async () => { + await db.withExclusiveTransactionAsync(async () => { + const result = await db.getAllAsync("SELECT * FROM items WHERE done = 0"); + setItems(result); + }); +}; +``` + +## Key API reference + +| Method | Purpose | +|--------|---------| +| `db.getAllAsync(sql, ...params)` | SELECT multiple rows | +| `db.getFirstAsync(sql, ...params)` | SELECT single row | +| `db.runAsync(sql, ...params)` | INSERT, UPDATE, DELETE | +| `db.execAsync(sql)` | Execute raw SQL (DDL, multiple statements) | +| `db.withExclusiveTransactionAsync(fn)` | Atomic transaction | + +## Migration pattern + +Use `PRAGMA user_version` for schema versioning: + +```tsx +if (currentVersion === 0) { + // Initial schema + currentVersion = 1; +} +if (currentVersion === 1) { + // Add new column + await db.execAsync("ALTER TABLE items ADD COLUMN category TEXT"); + currentVersion = 2; +} +await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); +``` + +## Adaptation notes + +- Merge dependencies — don't replace `package.json` +- Add `SQLiteProvider` as a wrapper in the existing layout hierarchy +- Adapt the schema to the user's actual data model +- The database file is stored in the app's document directory +- WAL journal mode (`PRAGMA journal_mode = 'wal'`) improves concurrent read performance +- SQLite works offline — no network required +- For remote/synced databases, see the `with-libsql` skill instead + +## Reference + +See full working example in this directory. diff --git a/with-storybook/SKILL.md b/with-storybook/SKILL.md new file mode 100644 index 00000000..44d22521 --- /dev/null +++ b/with-storybook/SKILL.md @@ -0,0 +1,177 @@ +--- +name: with-storybook +description: Add Storybook component documentation to an Expo project. Browse, test, and document React Native components in an interactive web UI. Use when the user wants Storybook, component documentation, visual testing, or a component library browser. +version: 1.0.0 +license: MIT +--- + +# Add Storybook Component Documentation + +## When to use + +- User wants to add Storybook for component development and documentation +- User asks about visual testing or component isolation +- User wants a browsable catalog of their React Native components + +## Dependencies + +```bash +npm install --save-dev @storybook/react @storybook/react-webpack5 @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links @storybook/addon-onboarding @storybook/addon-react-native-web @storybook/blocks @storybook/testing-library storybook +npm install react-dom react-native-web +``` + +## Configuration + +### 1. Create `.storybook/main.js` + +```js +/** @type { import('@storybook/react-webpack5').StorybookConfig } */ +const config = { + stories: [ + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", + ], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-onboarding", + "@storybook/addon-interactions", + { + name: "@storybook/addon-react-native-web", + options: { + modulesToTranspile: [], + projectRoot: "../", + }, + }, + ], + framework: { + name: "@storybook/react-webpack5", + options: {}, + }, + docs: { + autodocs: "tag", + }, +}; + +export default config; +``` + +### 2. Create `.storybook/preview.js` + +```js +/** @type { import('@storybook/react').Preview } */ +const preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +}; + +export default preview; +``` + +### 3. Add scripts to `package.json` + +```json +{ + "scripts": { + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + } +} +``` + +## Implementation + +### 1. Create a component + +Create `components/Button.jsx`: + +```jsx +import { Pressable, Text, StyleSheet } from "react-native"; + +export function Button({ label, onPress, primary }) { + return ( + + + {label} + + + ); +} + +const styles = StyleSheet.create({ + button: { padding: 12, borderRadius: 8, borderWidth: 1, borderColor: "#ccc" }, + primary: { backgroundColor: "#1ea7fd", borderColor: "#1ea7fd" }, + label: { textAlign: "center", fontSize: 16 }, + primaryLabel: { color: "white" }, +}); +``` + +### 2. Write a story + +Create `stories/Button.stories.jsx`: + +```jsx +import { Button } from "../components/Button"; + +export default { + title: "Components/Button", + component: Button, + argTypes: { + onPress: { action: "pressed" }, + }, +}; + +export const Primary = { + args: { + primary: true, + label: "Primary Button", + }, +}; + +export const Secondary = { + args: { + label: "Secondary Button", + }, +}; +``` + +### 3. Run Storybook + +```bash +npm run storybook +``` + +Opens at `http://localhost:6006`. + +## Key API reference + +| Concept | Purpose | +|---------|---------| +| `.stories.jsx` files | Define component variations with different props | +| `export default { title, component }` | Story metadata — title sets the sidebar hierarchy | +| `export const StoryName = { args }` | Named export = one story with specific props | +| `argTypes` | Configure controls panel (actions, color pickers, etc.) | +| `@storybook/addon-react-native-web` | Renders React Native components in the web Storybook UI | + +## Adaptation notes + +- Merge dependencies — don't replace `package.json` +- The `@storybook/addon-react-native-web` addon is key — it transpiles React Native components for the web-based Storybook UI +- Set `modulesToTranspile` in the addon config if using third-party RN libraries that need transpilation +- Place stories in a `stories/` directory (or update `main.js` to match your preferred location) +- Storybook runs as a separate web dev server — it does not affect the Expo app build +- Use `react-dom` and `react-native-web` as dev dependencies if they're not already in the project + +## Reference + +See full working example in this directory. diff --git a/with-stripe/SKILL.md b/with-stripe/SKILL.md new file mode 100644 index 00000000..bbe54291 --- /dev/null +++ b/with-stripe/SKILL.md @@ -0,0 +1,264 @@ +--- +name: with-stripe +description: Add Stripe payment processing to an Expo project. Supports native payment sheets (Apple Pay, Google Pay), web checkout, and server-side API routes. Use when the user wants payments, checkout, billing, subscriptions, or Stripe. +version: 1.0.0 +license: MIT +--- + +# Add Stripe Payments + +## When to use + +- User wants to accept payments in their Expo app +- User asks about Stripe, Apple Pay, Google Pay, or checkout +- User needs payment processing with both native and web support + +## Dependencies + +```bash +npx expo install @stripe/stripe-react-native expo-router expo-linking +npm install stripe @stripe/stripe-js @stripe/react-stripe-js +``` + +## Configuration + +### Environment variables + +Create or update `.env`: + +``` +STRIPE_SECRET_KEY=sk_test_... +EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... +``` + +Tell the user to get these from the Stripe Dashboard (https://dashboard.stripe.com/apikeys). + +### app.json + +Add the Stripe plugin and camera permission (for card scanning): + +```json +{ + "expo": { + "scheme": "", + "plugins": [ + "expo-router", + ["@stripe/stripe-react-native", { + "merchantIdentifier": "merchant.com.yourapp", + "publishableKey": "pk_test_..." + }] + ], + "ios": { + "infoPlist": { + "NSCameraUsageDescription": "Use the camera to scan cards." + } + }, + "web": { + "output": "server" + } + } +} +``` + +Set `web.output` to `"server"` for API routes to work. + +## Implementation + +### 1. Create Stripe server utility + +Create `utils/stripe-server.ts`: + +```tsx +import Stripe from "stripe"; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + httpClient: Stripe.createFetchHttpClient(), +}); +``` + +### 2. Create StripeProvider wrapper (platform-specific) + +**Native** (`components/stripe-provider.tsx`): + +```tsx +import { StripeProvider as NativeStripeProvider } from "@stripe/stripe-react-native"; +import * as Linking from "expo-linking"; + +export function StripeProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +**Web** (`components/stripe-provider.web.tsx`): + +```tsx +export function StripeProvider({ children }: { children: React.ReactNode }) { + return <>{children}; +} +``` + +### 3. Wrap app with StripeProvider + +In root layout (`app/_layout.tsx`): + +```tsx +import { StripeProvider } from "@/components/stripe-provider"; + +export default function RootLayout() { + return ( + + + + ); +} +``` + +### 4. Create payment API route + +Create `app/api/payment-sheet+api.ts`: + +```tsx +import { stripe } from "@/utils/stripe-server"; + +export async function POST(request: Request) { + const customer = await stripe.customers.create(); + const ephemeralKey = await stripe.ephemeralKeys.create( + { customer: customer.id }, + { apiVersion: "2024-06-20" } + ); + const paymentIntent = await stripe.paymentIntents.create({ + amount: 1099, + currency: "usd", + customer: customer.id, + automatic_payment_methods: { enabled: true }, + }); + + return Response.json({ + paymentIntent: paymentIntent.client_secret, + ephemeralKey: ephemeralKey.secret, + customer: customer.id, + publishableKey: process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY, + }); +} +``` + +### 5. Create checkout form (platform-specific) + +**Native** (`components/checkout-form.native.tsx`): + +```tsx +import { useStripe } from "@stripe/stripe-react-native"; +import * as Linking from "expo-linking"; +import { useState } from "react"; +import { Button, Alert } from "react-native"; + +export function CheckoutForm() { + const { initPaymentSheet, presentPaymentSheet } = useStripe(); + const [loading, setLoading] = useState(false); + + const openPaymentSheet = async () => { + setLoading(true); + const response = await fetch("/api/payment-sheet", { method: "POST" }); + const { paymentIntent, ephemeralKey, customer } = await response.json(); + + const { error: initError } = await initPaymentSheet({ + merchantDisplayName: "Your App", + customerId: customer, + customerEphemeralKeySecret: ephemeralKey, + paymentIntentClientSecret: paymentIntent, + returnURL: Linking.createURL("stripe-redirect"), + allowsDelayedPaymentMethods: true, + }); + + if (initError) { setLoading(false); return; } + + const { error } = await presentPaymentSheet(); + if (error) { + Alert.alert("Payment failed", error.message); + } else { + Alert.alert("Success", "Payment confirmed!"); + } + setLoading(false); + }; + + return