diff --git a/.agents/skills/clerk-backend-api/SKILL.md b/.agents/skills/clerk-backend-api/SKILL.md
new file mode 100644
index 0000000..5a3bbfc
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/SKILL.md
@@ -0,0 +1,426 @@
+---
+name: clerk-backend-api
+description: "Clerk Backend REST API explorer and executor. Browse tags, inspect endpoint schemas, and execute authenticated requests. Use when listing users, managing organizations, or calling any Clerk API endpoint."
+allowed-tools: Bash, Read, Grep, Skill, WebFetch
+license: MIT
+compatibility: Requires CLERK_SECRET_KEY (sk_*) for Backend API calls.
+---
+
+## Options context
+
+User Prompt: $ARGUMENTS
+
+## CRITICAL: Mandatory checks before EVERY write request
+
+Before ANY POST / PATCH / PUT / DELETE, you MUST do ALL of the following in your response:
+
+1. **Check CLERK_SECRET_KEY** — verify it is set:
+ ```bash
+ echo $CLERK_SECRET_KEY | head -c 10
+ ```
+ If empty, stop and ask the user. Do not proceed without a valid key.
+
+2. **Check CLERK_BAPI_SCOPES** — run:
+ ```bash
+ echo $CLERK_BAPI_SCOPES
+ ```
+ Inspect the output. If scopes are missing or do not include the required write permission, tell the user: *"This is a write operation and your current scopes may not allow it. Rerun with --admin to bypass?"* Do NOT attempt the request and fail — ask first.
+
+3. **For DELETE requests:** warn explicitly that the action is **IRREVERSIBLE** and list exactly what data will be permanently destroyed (user record, all sessions, all memberships, all associated data). Require explicit confirmation before proceeding. This warning is MANDATORY — never skip it.
+
+4. **For metadata operations:** always explain which metadata type is being used and why (see Metadata types section below).
+
+---
+
+## FAST PATH: Common operations (use directly, no spec fetching needed)
+
+For the operations below, skip spec fetching and execute immediately using these exact templates. Substitute `$CLERK_SECRET_KEY`, `$USER_ID`, `$ORG_ID`, `$EMAIL` as needed from the user's context.
+
+### Create organization + invite member (two-step)
+
+```bash
+# Step 1 — Create organization
+ORG=$(curl -s -X POST "https://api.clerk.com/v1/organizations" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\": \"Acme Corp\", \"created_by\": \"$USER_ID\"}")
+echo "$ORG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d, indent=2))"
+
+# Step 2 — Extract org ID
+ORG_ID=$(echo "$ORG" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
+
+# Step 3 — Invite member with role
+curl -s -X POST "https://api.clerk.com/v1/organizations/${ORG_ID}/invitations" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ -H "Content-Type: application/json" \
+ -d "{\"email_address\": \"user@example.com\", \"role\": \"org:admin\"}" \
+ | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))"
+```
+
+**Roles:** use `"org:admin"` or `"org:member"` (always prefix with `org:`).
+
+### SDK equivalent (for Next.js / TypeScript projects with `@clerk/nextjs` or `@clerk/backend`)
+
+```typescript
+import { clerkClient } from '@clerk/nextjs/server'
+// OR if using @clerk/backend directly:
+// import { createClerkClient } from '@clerk/backend'
+// const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })
+
+// Step 1: Create organization
+const org = await clerkClient.organizations.createOrganization({
+ name: 'Acme Corp',
+ createdBy: userId, // required — the ID of the user creating the org
+})
+
+// Step 2: Invite member to the org
+const invitation = await clerkClient.organizations.createOrganizationInvitation({
+ organizationId: org.id,
+ emailAddress: 'user@example.com',
+ role: 'org:admin', // or 'org:member'
+})
+```
+
+### Update user metadata
+
+**Always explain the three metadata types before asking which to use:**
+
+| Type | Field | Readable by | Writable by | Use for |
+|------|-------|-------------|-------------|---------|
+| Public | `public_metadata` | Client + Server | **Server only** | Plan tier, roles, feature flags the frontend reads |
+| Private | `private_metadata` | **Server only** | **Server only** | Stripe IDs, compliance flags, internal identifiers |
+| Unsafe | `unsafe_metadata` | Client + Server | Client + Server | Ephemeral UI state, onboarding steps (client-writable — avoid sensitive data) |
+
+**For `plan: 'pro'` and `onboarded: true` — use `public_metadata`** (frontend-readable, server-writable):
+
+```bash
+curl -s -X PATCH "https://api.clerk.com/v1/users/${USER_ID}" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"public_metadata": {"plan": "pro", "onboarded": true}}' \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Updated user {d[\"id\"]}: public_metadata={d.get(\"public_metadata\")}')"
+```
+
+**SDK equivalent:**
+
+```typescript
+import { clerkClient } from '@clerk/nextjs/server'
+// OR: import { createClerkClient } from '@clerk/backend'
+
+await clerkClient.users.updateUser(userId, {
+ publicMetadata: { plan: 'pro', onboarded: true }, // readable by client, writable server-only
+ // privateMetadata: { stripeId: 'cus_xxx' }, // server-only read AND write
+ // unsafeMetadata: { step: 'welcome' }, // client-writable, avoid sensitive data
+})
+```
+
+**Note:** REST API uses `snake_case` (`public_metadata`). SDK uses `camelCase` (`publicMetadata`).
+
+### List users (last 7 days)
+
+```bash
+curl -s "https://api.clerk.com/v1/users?limit=100&offset=0&order_by=-created_at&created_at=gt:$(date -d '7 days ago' +%s 2>/dev/null || date -v-7d +%s)000" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ | python3 -c "
+import sys, json
+data = json.load(sys.stdin)
+if isinstance(data, list):
+ print(f'Found {len(data)} users:')
+ for u in data:
+ print(f' {u[\"id\"]}: {u.get(\"email_addresses\", [{}])[0].get(\"email_address\", \"no email\")}')
+else:
+ print(json.dumps(data, indent=2))
+"
+```
+
+### Delete user (confirm required)
+
+```bash
+# ONLY run after explicit user confirmation
+curl -s -X DELETE "https://api.clerk.com/v1/users/${USER_ID}" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Deleted: {d}')"
+```
+
+---
+
+## Clerk Backend API — Full Endpoint Reference
+
+Base URL: `https://api.clerk.com/v1`
+Auth: `Authorization: Bearer $CLERK_SECRET_KEY` on every request.
+
+### Users
+
+**List users**
+```
+GET /v1/users
+Query params: limit (max 500, default 10), offset, order_by (+/-created_at, +/-updated_at, +/-email_address, +/-web3wallet, +/-first_name, +/-last_name, +/-phone_number, +/-username, +/-last_active_at, +/-last_sign_in_at), email_address[], phone_number[], username[], web3wallet[], user_id[], query, created_at (ISO 8601 range: gt:TIMESTAMP or lt:TIMESTAMP in Unix ms)
+Returns: array of User objects
+```
+
+**Get user**
+```
+GET /v1/users/{user_id}
+Returns: User object
+```
+
+**Update user**
+```
+PATCH /v1/users/{user_id}
+Body (JSON, snake_case): { public_metadata, private_metadata, unsafe_metadata, first_name, last_name, username, ... }
+```
+
+**Delete user — IRREVERSIBLE**
+```
+DELETE /v1/users/{user_id}
+Destroys: user record, all sessions, all memberships, all associated data
+Returns: { id, object, deleted: true }
+```
+Always warn the user this is permanent and confirm before proceeding.
+
+### Organizations
+
+**Create organization**
+```
+POST /v1/organizations
+Body: { name: string, created_by: string (user_id), public_metadata?, private_metadata?, max_allowed_memberships? }
+Returns: Organization object with { id, name, slug, ... }
+```
+
+**List organizations**
+```
+GET /v1/organizations
+Query params: limit, offset, query, order_by
+```
+
+**Invite member**
+```
+POST /v1/organizations/{organization_id}/invitations
+Body: { email_address: string, role: string ("org:admin" or "org:member"), public_metadata?, private_metadata? }
+Returns: OrganizationInvitation object
+```
+
+---
+
+## How to execute requests
+
+**ALWAYS execute requests with direct `curl` commands.** Use the spec-extraction scripts (`api-specs-context.sh`, `extract-tags.js`, `extract-endpoint-detail.sh`) to discover endpoints, but make actual API calls with `curl`. Do NOT use `scripts/execute-request.sh` — it's a local dev helper, not for agent use.
+
+Template for GET requests:
+```bash
+curl -s "https://api.clerk.com/v1${PATH}${QUERY_STRING}" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY"
+```
+
+Template for POST/PATCH requests:
+```bash
+curl -s -X ${METHOD} "https://api.clerk.com/v1${PATH}" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
+ -H "Content-Type: application/json" \
+ -d '${BODY_JSON}'
+```
+
+Template for DELETE requests:
+```bash
+curl -s -X DELETE "https://api.clerk.com/v1${PATH}" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY"
+```
+
+**After getting the response:** Parse and display it clearly. Use `python3 -c "import sys,json; data=json.load(sys.stdin); print(json.dumps(data, indent=2))"` to pretty-print JSON. Extract key fields (id, email, name, etc.) and summarize them for the user.
+
+---
+
+## API specs context
+
+Before doing anything outside the FAST PATH, fetch the available spec versions and tags by running:
+```bash
+bash scripts/api-specs-context.sh
+```
+
+Use the output to determine the latest version and available tags.
+
+**Caching:** If you already fetched the spec context earlier in this conversation, do NOT fetch it again. Reuse the version and tags from the previous call.
+
+---
+
+## Rules
+
+- For common operations (list users, create org, invite, update metadata, delete user): use the FAST PATH above — do NOT fetch specs first.
+- Always disregard endpoints/schemas related to `platform`.
+- Always confirm before performing write requests (POST/PUT/PATCH/DELETE).
+- For DELETE operations, always warn the user that the action is **irreversible** and mention what data will be lost (user record, sessions, memberships). This warning is MANDATORY — never skip it.
+- For write operations (POST/PUT/PATCH/DELETE), check `CLERK_BAPI_SCOPES` before attempting the request. If missing or insufficient, ask the user upfront. Do NOT attempt and fail — ask before executing. This check is MANDATORY.
+- For metadata operations, always explain all three types (public, private, unsafe) and recommend the appropriate one.
+- Pagination: always use `limit` + `offset` and mention that results may be paginated for large datasets.
+- Use direct curl commands for all API calls — never use `scripts/execute-request.sh`.
+
+---
+
+## Rate Limits & Gotchas
+
+### Rate Limits
+
+| Environment | Limit |
+|-------------|-------|
+| Production | 1,000 requests / 10 seconds |
+| Development | 100 requests / 10 seconds |
+| Single invitations | 100 / hour |
+| Bulk invitations | 25 / hour |
+| Org invitations | 250 / hour |
+| Frontend API sign-in creation | 5 / 10 seconds |
+| Frontend API sign-in attempts | 3 / 10 seconds |
+| List users max per page | 500 |
+
+`currentUser()` makes a real API call that counts against rate limits. Use `auth()` for just the session claims — it reads from the token without an API call.
+
+### Metadata Overwrites (Not Merges)
+
+`updateUser({ publicMetadata: { role: 'admin' } })` REPLACES all public metadata, not merges. To add a field without losing existing data: read first, spread, then write.
+
+Wrong:
+```typescript
+await clerkClient.users.updateUser(userId, { publicMetadata: { newField: 'value' } })
+```
+This DELETES all other `publicMetadata` fields.
+
+Right:
+```typescript
+const user = await clerkClient.users.getUser(userId)
+await clerkClient.users.updateUser(userId, {
+ publicMetadata: { ...user.publicMetadata, newField: 'value' },
+})
+```
+
+---
+
+## Modes
+
+Determine the active mode based on the user prompt in [Options context](#options-context):
+
+| Mode | Trigger | Behavior |
+|------|---------|----------|
+| `help` | Prompt is empty, or contains only `help` / `-h` / `--help` | Print usage examples (step 0) |
+| `browse` | Prompt is `tags`, or a tag name (e.g. `Users`) | List all tags or endpoints for a tag |
+| `execute` | Specific endpoint (e.g. `GET /users`) or natural language action (e.g. "get user john_doe") | Look up endpoint, execute request |
+| `detail` | Endpoint + `help` / `-h` / `--help` (e.g. `GET /users help`) | Show endpoint schema, don't execute |
+
+---
+
+## Your Task
+
+Use the **LATEST VERSION** from [API specs context](#api-specs-context) by default. If the user specifies a different version (e.g. `--version 2024-10-01`), use that version instead.
+
+Determine the active mode, then follow the applicable steps below.
+
+---
+
+### 0. Print usage
+
+**Modes:** `help` only — **Skip** for `browse`, `execute`, and `detail`.
+
+Print the following examples to the user verbatim:
+
+```
+Browse
+ /clerk-backend-api tags — list all tags
+ /clerk-backend-api Users — browse endpoints for the Users tag
+ /clerk-backend-api Users version 2025-11-10.yml — browse using a different version
+
+Execute
+ /clerk-backend-api GET /users — fetch all users
+ /clerk-backend-api get user john_doe — natural language works too
+ /clerk-backend-api POST /invitations — create an invitation
+
+Inspect
+ /clerk-backend-api GET /users help — show endpoint schema without executing
+ /clerk-backend-api POST /invitations -h — view request/response details
+
+Options
+ --admin — bypass scope restrictions for write/delete
+ --version [date], version [date] — use a specific spec version
+ --help, -h, help — inspect endpoint instead of executing
+```
+
+Stop here.
+
+---
+
+### 1. Fetch tags
+
+**Modes:** `browse` (when prompt is `tags` or no tag specified) — **Skip** for `help`, `execute`, and `detail`.
+
+If using a non-latest version, fetch tags for that version:
+```bash
+curl -s https://raw.githubusercontent.com/clerk/openapi-specs/main/bapi/${version_name} | node scripts/extract-tags.js
+```
+Otherwise, use the **TAGS** already in [API specs context](#api-specs-context).
+
+Share tags in a table and prompt the user to select a query.
+
+---
+
+### 2. Fetch tag endpoints
+
+**Modes:** `browse` (when a tag name is provided) — **Skip** for `help`, `execute`, and `detail`.
+
+Fetch all endpoints for the identified tag:
+```bash
+curl -s https://raw.githubusercontent.com/clerk/openapi-specs/main/bapi/${version_name} | bash scripts/extract-tag-endpoints.sh "${tag_name}"
+```
+
+Share the results (endpoints, schemas, parameters) with the user.
+
+---
+
+### 3. Fetch endpoint detail
+
+**Modes:** `execute`, `detail` — **Skip** for `help` and `browse`.
+
+For natural language prompts in `execute` mode, first check if the operation matches a FAST PATH entry above. If it does, skip this step and proceed directly to step 4 using the FAST PATH template.
+
+For other endpoints, identify the matching endpoint by searching the tags in context. Fetch tag endpoints if needed to resolve the exact path and method.
+
+Extract the full endpoint definition:
+```bash
+curl -s https://raw.githubusercontent.com/clerk/openapi-specs/main/bapi/${version_name} | bash scripts/extract-endpoint-detail.sh "${path}" "${method}"
+```
+- `${path}` — e.g. `/users/{user_id}`
+- `${method}` — lowercase, e.g. `get`
+
+**`detail` mode:** Share the endpoint definition and schemas with the user. Stop here.
+
+**`execute` mode:** Continue to step 4.
+
+---
+
+### 4. Execute request
+
+**Modes:** `execute` only.
+
+1. Run the **mandatory checks** from the CRITICAL section above.
+2. Identify required and optional parameters from the spec (step 3) or FAST PATH.
+3. Ask the user for any required path/query/body parameters that weren't provided.
+4. Build and execute a **direct curl command** (see How to execute requests above). Do NOT use `scripts/execute-request.sh`.
+5. Parse the JSON response and display it clearly. Extract and summarize key fields for the user.
+
+**Example — list users and parse response:**
+```bash
+RESPONSE=$(curl -s "https://api.clerk.com/v1/users?limit=10" \
+ -H "Authorization: Bearer $CLERK_SECRET_KEY")
+echo "$RESPONSE" | python3 -c "
+import sys, json
+data = json.load(sys.stdin)
+if isinstance(data, list):
+ print(f'Found {len(data)} users:')
+ for u in data:
+ print(f' {u[\"id\"]}: {u.get(\"email_addresses\", [{}])[0].get(\"email_address\", \"no email\")}')
+else:
+ print(json.dumps(data, indent=2))
+"
+```
+
+## See Also
+
+- `clerk-setup` - Initial Clerk install
+- `clerk-orgs` - Manage organizations via API
+- `clerk-webhooks` - Real-time event sync
diff --git a/.agents/skills/clerk-backend-api/evals/evals.json b/.agents/skills/clerk-backend-api/evals/evals.json
new file mode 100644
index 0000000..9013dd9
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/evals/evals.json
@@ -0,0 +1,87 @@
+{
+ "skill_name": "clerk-backend-api",
+ "evals": [
+ {
+ "id": 1,
+ "prompt": "i need to get all users who signed up in the last 7 days from clerk's backend API. show me how",
+ "expected_output": "REST API call to GET /v1/users with date filter, Bearer token auth using CLERK_SECRET_KEY, response parsing and pagination handling",
+ "scaffold": "nextjs-basic-auth",
+ "files": [],
+ "expectations": [
+ "Uses GET /v1/users endpoint",
+ "Includes a created_at or date-based filter query parameter to narrow to last 7 days",
+ "Sets CLERK_SECRET_KEY as Bearer token in the Authorization header",
+ "Parses the JSON response array of user objects",
+ "Handles or mentions pagination via limit and offset parameters"
+ ]
+ },
+ {
+ "id": 2,
+ "prompt": "use the clerk backend api to create a new organization called 'Acme Corp' and invite user@example.com as an admin",
+ "expected_output": "Two API calls: POST /v1/organizations to create org, then POST /v1/organizations/{id}/invitations to invite with admin role",
+ "scaffold": "nextjs-basic-auth",
+ "files": [],
+ "expectations": [
+ "Uses POST /v1/organizations to create the organization with name 'Acme Corp'",
+ "Uses POST /v1/organizations/{id}/invitations to send the invitation",
+ "Sets the role to admin (or org:admin) in the invitation payload",
+ "Authenticates both API calls using CLERK_SECRET_KEY in the Authorization header",
+ "Extracts the organization ID from the first response and uses it in the invitation request"
+ ]
+ },
+ {
+ "id": 3,
+ "prompt": "i need to set custom metadata on a user via clerk's backend api. set their plan to 'pro' and onboarded to true",
+ "expected_output": "PATCH /v1/users/{user_id} with public_metadata or private_metadata body, explanation of metadata types and when to use each",
+ "scaffold": "nextjs-basic-auth",
+ "files": [],
+ "expectations": [
+ "Uses PATCH /v1/users/{user_id} endpoint",
+ "Sets the metadata in the correct field: public_metadata, private_metadata, or unsafe_metadata",
+ "Explains the difference between metadata types (client-readable vs server-only vs client-writable)",
+ "Includes a properly formed JSON body with the plan and onboarded fields",
+ "Mentions checking write permissions before executing the update"
+ ]
+ },
+ {
+ "id": 4,
+ "prompt": "remove a user with id user_abc123 from our clerk instance using the backend api",
+ "expected_output": "DELETE /v1/users/{user_id} call with warning about irreversibility, CLERK_BAPI_SCOPES check, and CLERK_SECRET_KEY auth",
+ "scaffold": "nextjs-basic-auth",
+ "files": [],
+ "expectations": [
+ "Uses DELETE /v1/users/{user_id} endpoint with the correct user ID",
+ "Warns that the action is destructive and irreversible",
+ "Requires or recommends explicit confirmation before executing the delete",
+ "Includes CLERK_SECRET_KEY as Bearer token in the Authorization header",
+ "Mentions implications such as loss of user data, sessions, and associated records"
+ ]
+ },
+ {
+ "id": 5,
+ "prompt": "i'm updating user metadata to add a 'plan' field but it keeps deleting the existing 'role' field. what am i doing wrong?",
+ "expected_output": "Metadata updates overwrite not merge. Read existing metadata first, spread it, then write back.",
+ "scaffold": "nextjs-basic-auth",
+ "expectations": [
+ "Explains that updateUser publicMetadata replaces all existing metadata, not merges",
+ "Shows reading the existing user first with getUser(userId)",
+ "Spreads existing metadata before adding new fields ({ ...user.publicMetadata, plan: 'pro' })",
+ "Calls updateUser with the merged metadata object",
+ "Warns that this applies to publicMetadata, privateMetadata, and unsafeMetadata equally"
+ ]
+ },
+ {
+ "id": 6,
+ "prompt": "my clerk backend api calls are failing with 429 errors in development. what are the rate limits?",
+ "expected_output": "Development has 100 req/10s limit (vs 1000/10s in production). Batch operations or reduce call frequency.",
+ "scaffold": "nextjs-basic-auth",
+ "expectations": [
+ "States the development rate limit (100 requests per 10 seconds)",
+ "States the production rate limit (1000 requests per 10 seconds)",
+ "Suggests batching operations or reducing call frequency",
+ "Mentions that currentUser() counts as an API call against the limit",
+ "Recommends using auth() instead of currentUser() when only session claims are needed"
+ ]
+ }
+ ]
+}
diff --git a/.agents/skills/clerk-backend-api/scripts/api-specs-context.sh b/.agents/skills/clerk-backend-api/scripts/api-specs-context.sh
new file mode 100644
index 0000000..7015d1b
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/scripts/api-specs-context.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+# Fetches all available BAPI spec versions, determines the latest,
+# and extracts tags from it. Output is used as skill context.
+
+set -euo pipefail
+
+API_URL="https://api.github.com/repos/clerk/openapi-specs/contents/bapi"
+RAW_BASE="https://raw.githubusercontent.com/clerk/openapi-specs/main/bapi"
+
+# Fetch version list, parse dates, sort, pick latest
+versions=$(curl -s "$API_URL" | node -e "
+ let d='';
+ process.stdin.on('data',c=>d+=c);
+ process.stdin.on('end',()=>{
+ const items = JSON.parse(d)
+ .map(i=>i.name)
+ .filter(n=>/^\d{4}-\d{2}-\d{2}\.yml$/.test(n))
+ .sort();
+ items.forEach(n=>console.log(n));
+ });
+")
+
+latest=$(echo "$versions" | tail -1)
+
+echo "AVAILABLE VERSIONS: $(echo "$versions" | tr '\n' ' ')"
+echo "LATEST VERSION: $latest"
+echo ""
+echo "TAGS:"
+curl -s "${RAW_BASE}/${latest}" | node "$(dirname "$0")/extract-tags.js"
diff --git a/.agents/skills/clerk-backend-api/scripts/execute-request.sh b/.agents/skills/clerk-backend-api/scripts/execute-request.sh
new file mode 100644
index 0000000..26dcb43
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/scripts/execute-request.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+
+# Execute a Clerk Backend API request with scope enforcement.
+#
+# Usage: bash execute-request.sh [--admin] [BODY]
+#
+# Scope enforcement:
+# GET — always allowed
+# POST, PUT, PATCH — requires CLERK_BAPI_SCOPES="write" or --admin flag
+# DELETE — requires CLERK_BAPI_SCOPES="write,delete" or --admin flag
+
+set -euo pipefail
+
+# Walk up from $PWD to find .env/.env.local (mirrors Clerk CLI behavior).
+# Stops at the first directory that provides CLERK_SECRET_KEY.
+_dir="$PWD"
+while true; do
+ for _envfile in "$_dir/.env" "$_dir/.env.local"; do
+ if [[ -f "$_envfile" ]]; then
+ set -a
+ source "$_envfile"
+ set +a
+ fi
+ done
+ [[ -n "${CLERK_SECRET_KEY:-}" ]] && break
+ _parent="$(dirname "$_dir")"
+ [[ "$_parent" == "$_dir" ]] && break
+ _dir="$_parent"
+done
+unset _dir _parent _envfile
+
+# Parse --admin flag
+ADMIN=false
+if [[ "${1:-}" == "--admin" ]]; then
+ ADMIN=true
+ shift
+fi
+
+METHOD="${1:?Usage: execute-request.sh [--admin] [BODY]}"
+PATH_ARG="${2:?Usage: execute-request.sh [--admin] [BODY]}"
+BODY="${3:-}"
+
+METHOD_UPPER=$(echo "$METHOD" | tr '[:lower:]' '[:upper:]')
+SCOPES="${CLERK_BAPI_SCOPES:-}"
+
+# Scope check
+if [[ "$ADMIN" == false ]]; then
+ case "$METHOD_UPPER" in
+ GET)
+ ;; # always allowed
+ POST|PUT|PATCH)
+ if [[ "$SCOPES" != *"write"* ]]; then
+ echo "ERROR: $METHOD_UPPER requests require CLERK_BAPI_SCOPES=\"write\" or --admin flag." >&2
+ echo "Current CLERK_BAPI_SCOPES: \"$SCOPES\"" >&2
+ exit 1
+ fi
+ ;;
+ DELETE)
+ if [[ "$SCOPES" != *"write"* ]] || [[ "$SCOPES" != *"delete"* ]]; then
+ echo "ERROR: DELETE requests require CLERK_BAPI_SCOPES=\"write,delete\" or --admin flag." >&2
+ echo "Current CLERK_BAPI_SCOPES: \"$SCOPES\"" >&2
+ exit 1
+ fi
+ ;;
+ *)
+ echo "ERROR: Unknown HTTP method: $METHOD_UPPER" >&2
+ exit 1
+ ;;
+ esac
+fi
+
+# Base URL: use CLERK_REST_API_URL if set, otherwise default to production
+BASE_URL="${CLERK_REST_API_URL:-https://api.clerk.com}"
+
+# Build curl command
+CURL_ARGS=(
+ -s
+ -X "$METHOD_UPPER"
+ "${BASE_URL}/v1${PATH_ARG}"
+ -H "Authorization: Bearer ${CLERK_SECRET_KEY:?CLERK_SECRET_KEY is not set}"
+ -H "Content-Type: application/json"
+)
+
+if [[ -n "$BODY" ]]; then
+ CURL_ARGS+=(-d "$BODY")
+fi
+
+curl "${CURL_ARGS[@]}"
diff --git a/.agents/skills/clerk-backend-api/scripts/extract-endpoint-detail.sh b/.agents/skills/clerk-backend-api/scripts/extract-endpoint-detail.sh
new file mode 100644
index 0000000..19eb632
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/scripts/extract-endpoint-detail.sh
@@ -0,0 +1,165 @@
+#!/usr/bin/env bash
+# extract-endpoint-detail.sh
+#
+# Extracts full details for a specific endpoint from an OpenAPI YAML spec (stdin),
+# including parameters, request body, responses, and all referenced component schemas.
+#
+# Usage:
+# curl -s | bash extract-endpoint-detail.sh "/users/{user_id}/billing/subscription" "get"
+
+set -euo pipefail
+
+ENDPOINT="${1:?Usage: extract-endpoint-detail.sh }"
+METHOD="${2:?Usage: extract-endpoint-detail.sh }"
+TMPDIR_WORK=$(mktemp -d)
+trap 'rm -rf "$TMPDIR_WORK"' EXIT
+
+SPEC="$TMPDIR_WORK/spec.yml"
+cat > "$SPEC"
+
+node - "$ENDPOINT" "$METHOD" "$SPEC" <<'SCRIPT'
+const fs = require("fs");
+const endpoint = process.argv[2];
+const method = process.argv[3].toLowerCase();
+const specFile = process.argv[4];
+const lines = fs.readFileSync(specFile, "utf8").split("\n");
+
+const httpMethods = ["get", "post", "put", "patch", "delete", "options", "head"];
+
+// Locate paths: and components: sections
+let pathsStart = -1, pathsEnd = -1, componentsStart = -1;
+for (let i = 0; i < lines.length; i++) {
+ if (/^paths:\s*$/.test(lines[i])) pathsStart = i;
+ else if (pathsStart >= 0 && pathsEnd < 0 && /^\S/.test(lines[i]) && i > pathsStart) pathsEnd = i;
+ if (/^components:\s*$/.test(lines[i])) componentsStart = i;
+}
+if (pathsEnd < 0) pathsEnd = lines.length;
+
+// Find the target path + method block
+let targetStart = -1, targetEnd = -1;
+let currentPath = null;
+
+for (let i = pathsStart + 1; i < pathsEnd; i++) {
+ const line = lines[i];
+
+ // Path line: exactly 2 spaces + /
+ if (/^ {2}\/\S/.test(line)) {
+ currentPath = line.trim().replace(/:$/, "");
+ continue;
+ }
+
+ // Method line: exactly 4 spaces + method name
+ const methodMatch = line.match(/^ {4}(\w+):\s*$/);
+ if (methodMatch && httpMethods.includes(methodMatch[1])) {
+ if (currentPath === endpoint && methodMatch[1] === method) {
+ targetStart = i;
+ // Find end of this method block
+ for (let j = i + 1; j < pathsEnd; j++) {
+ const nextLine = lines[j];
+ // New method or new path
+ if (/^ {2}\/\S/.test(nextLine) || (/^ {4}\w+:\s*$/.test(nextLine) && httpMethods.some(m => nextLine.trim().startsWith(m + ":")))) {
+ targetEnd = j;
+ break;
+ }
+ }
+ if (targetEnd < 0) targetEnd = pathsEnd;
+ break;
+ }
+ }
+}
+
+if (targetStart < 0) {
+ console.error(`Endpoint not found: ${method.toUpperCase()} ${endpoint}`);
+ process.exit(1);
+}
+
+const blockLines = lines.slice(targetStart, targetEnd);
+
+// Collect all $refs from the block
+const allRefs = new Set();
+for (const bl of blockLines) {
+ const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/);
+ if (refMatch) allRefs.add(refMatch[1]);
+}
+
+// Resolve a $ref path to the raw YAML lines for that component
+function resolveRef(ref) {
+ const parts = ref.replace("#/", "").split("/");
+ // Find the component in the file by walking indentation
+ let searchStart = 0;
+ for (let p = 0; p < parts.length; p++) {
+ const indent = p * 2;
+ const target = " ".repeat(indent) + parts[p] + ":";
+ let found = false;
+ for (let i = searchStart; i < lines.length; i++) {
+ if (lines[i].startsWith(target) && (lines[i] === target || lines[i][target.length] === " ")) {
+ searchStart = i + 1;
+ found = true;
+ break;
+ }
+ }
+ if (!found) return null;
+ }
+
+ // Collect lines for this component (until same or lower indent)
+ const componentStart = searchStart - 1;
+ const baseIndent = parts.length * 2;
+ const result = [];
+ for (let i = searchStart; i < lines.length; i++) {
+ const line = lines[i];
+ if (line.trim() === "") { result.push(line); continue; }
+ const lineIndent = line.length - line.trimStart().length;
+ if (lineIndent < baseIndent) break;
+ result.push(line);
+ }
+ return result;
+}
+
+// Recursively resolve refs from component bodies
+function collectDeepRefs(refSet, visited) {
+ const toProcess = [...refSet].filter(r => !visited.has(r));
+ for (const ref of toProcess) {
+ visited.add(ref);
+ const body = resolveRef(ref);
+ if (!body) continue;
+ for (const bl of body) {
+ const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/);
+ if (refMatch && !visited.has(refMatch[1])) {
+ refSet.add(refMatch[1]);
+ }
+ }
+ }
+ // Recurse if new refs were found
+ const newRefs = [...refSet].filter(r => !visited.has(r));
+ if (newRefs.length > 0) collectDeepRefs(refSet, visited);
+}
+
+collectDeepRefs(allRefs, new Set());
+
+// Output
+console.log(`## \`${method.toUpperCase()}\` \`${endpoint}\`\n`);
+console.log("### Endpoint Definition\n");
+console.log("```yaml");
+for (const bl of blockLines) {
+ console.log(bl);
+}
+console.log("```\n");
+
+if (allRefs.size > 0) {
+ console.log(`### Referenced Components (${allRefs.size})\n`);
+ const sorted = [...allRefs].sort();
+ for (const ref of sorted) {
+ const name = ref.split("/").pop();
+ const category = ref.replace("#/", "").split("/").slice(0, -1).join("/");
+ console.log(`#### \`${name}\` (${category})\n`);
+ const body = resolveRef(ref);
+ if (body) {
+ console.log("```yaml");
+ for (const bl of body) console.log(bl);
+ console.log("```\n");
+ } else {
+ console.log("_(could not resolve)_\n");
+ }
+ }
+}
+SCRIPT
diff --git a/.agents/skills/clerk-backend-api/scripts/extract-tag-endpoints.sh b/.agents/skills/clerk-backend-api/scripts/extract-tag-endpoints.sh
new file mode 100644
index 0000000..5076498
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/scripts/extract-tag-endpoints.sh
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+# extract-tag-endpoints.sh
+#
+# Extracts all endpoints for a given tag from an OpenAPI YAML spec (stdin),
+# along with any $ref'd schemas/components.
+#
+# Usage:
+# curl -s | bash extract-tag-endpoints.sh "Billing"
+
+set -euo pipefail
+
+TAG="${1:?Usage: extract-tag-endpoints.sh }"
+TMPDIR_WORK=$(mktemp -d)
+trap 'rm -rf "$TMPDIR_WORK"' EXIT
+
+SPEC="$TMPDIR_WORK/spec.yml"
+cat > "$SPEC"
+
+# 1. Find all path+method blocks that have a matching tag
+# Strategy: find line numbers of path entries (lines starting with " /"),
+# then for each method block under that path, check if it contains the tag.
+
+node - "$TAG" "$SPEC" <<'SCRIPT'
+const fs = require("fs");
+const tag = process.argv[2];
+const specFile = process.argv[3];
+const lines = fs.readFileSync(specFile, "utf8").split("\n");
+
+const tagLower = tag.toLowerCase();
+
+// Phase 1: Find all path definitions and their method blocks
+// Paths start at indent 2 with " /"
+// Methods start at indent 4 with " get:", " post:", etc.
+const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
+const endpoints = [];
+const refs = new Set();
+
+let currentPath = null;
+let currentMethod = null;
+let blockStart = -1;
+let blockLines = [];
+let inPaths = false;
+let inComponents = false;
+
+// First pass: locate the "paths:" and "components:" top-level keys
+let pathsStart = -1;
+let pathsEnd = -1;
+let componentsStart = -1;
+
+for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (/^paths:\s*$/.test(line)) {
+ pathsStart = i;
+ } else if (pathsStart >= 0 && pathsEnd < 0 && /^\S/.test(line) && i > pathsStart) {
+ pathsEnd = i;
+ }
+ if (/^components:\s*$/.test(line)) {
+ componentsStart = i;
+ }
+}
+if (pathsEnd < 0) pathsEnd = lines.length;
+
+// Second pass: extract endpoints matching the tag
+function flushBlock() {
+ if (!currentPath || !currentMethod || blockLines.length === 0) return;
+
+ // Check if this block has the target tag
+ let inTags = false;
+ let hasTag = false;
+ const blockRefs = [];
+
+ for (const bl of blockLines) {
+ const trimmed = bl.trim();
+
+ // Detect tags section
+ if (/^tags:\s*$/.test(trimmed)) {
+ inTags = true;
+ continue;
+ }
+ if (inTags) {
+ if (/^- /.test(trimmed)) {
+ const tagVal = trimmed.replace(/^- /, "").trim().replace(/^['"]|['"]$/g, "");
+ if (tagVal.toLowerCase() === tagLower) hasTag = true;
+ } else {
+ inTags = false;
+ }
+ }
+
+ // Collect $ref values
+ const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/);
+ if (refMatch) blockRefs.push(refMatch[1]);
+ }
+
+ if (hasTag) {
+ // Extract summary, operationId, description
+ let summary = "";
+ let operationId = "";
+ let description = "";
+ let params = [];
+ let inDesc = false;
+ let inParams = false;
+
+ for (const bl of blockLines) {
+ const trimmed = bl.trim();
+ const indent = bl.length - bl.trimStart().length;
+
+ // Only capture operation-level keys (indent 6 = direct children of the method block)
+ if (indent === 6) {
+ const sumMatch = trimmed.match(/^summary:\s*(.+)/);
+ if (sumMatch) summary = sumMatch[1].replace(/^['"]|['"]$/g, "");
+
+ const opMatch = trimmed.match(/^operationId:\s*(.+)/);
+ if (opMatch) operationId = opMatch[1].replace(/^['"]|['"]$/g, "");
+
+ const descMatch = trimmed.match(/^description:\s*(.+)/);
+ if (descMatch && !inDesc) {
+ const val = descMatch[1].trim();
+ if (val === "|-" || val === "|" || val === ">-" || val === ">") {
+ inDesc = true;
+ } else {
+ description = val.replace(/^['"]|['"]$/g, "");
+ }
+ continue;
+ }
+ }
+
+ if (inDesc) {
+ // Continuation lines of description — grab first non-empty line
+ if (!description && trimmed.length > 0) {
+ description = trimmed;
+ }
+ // Stop when we hit the next operation-level key
+ if (indent === 6 && trimmed.length > 0 && !/^description:/.test(trimmed)) {
+ inDesc = false;
+ }
+ }
+ }
+
+ endpoints.push({
+ method: currentMethod.toUpperCase(),
+ path: currentPath,
+ operationId,
+ summary,
+ description,
+ refs: blockRefs,
+ });
+
+ for (const r of blockRefs) refs.add(r);
+ }
+}
+
+for (let i = pathsStart + 1; i < pathsEnd; i++) {
+ const line = lines[i];
+
+ // Path line: exactly 2 spaces + /
+ if (/^ {2}\/\S/.test(line)) {
+ flushBlock();
+ currentPath = line.trim().replace(/:$/, "");
+ currentMethod = null;
+ blockLines = [];
+ continue;
+ }
+
+ // Method line: exactly 4 spaces + method name
+ const methodMatch = line.match(/^ {4}(\w+):\s*$/);
+ if (methodMatch && methods.includes(methodMatch[1])) {
+ flushBlock();
+ currentMethod = methodMatch[1];
+ blockLines = [];
+ continue;
+ }
+
+ if (currentMethod) {
+ blockLines.push(line);
+ }
+}
+flushBlock();
+
+// Output endpoints
+if (endpoints.length === 0) {
+ console.error(`No endpoints found for tag: "${tag}"`);
+ process.exit(1);
+}
+
+console.log(`## Endpoints for "${tag}" (${endpoints.length} total)\n`);
+for (const ep of endpoints) {
+ console.log(`### \`${ep.method}\` \`${ep.path}\``);
+ if (ep.operationId) console.log(`- **operationId**: \`${ep.operationId}\``);
+ if (ep.summary) console.log(`- **summary**: ${ep.summary}`);
+ if (ep.description && ep.description !== ep.summary)
+ console.log(`- **description**: ${ep.description}`);
+ if (ep.refs.length > 0) {
+ console.log(`- **refs**: ${ep.refs.map(r => "\`" + r.split("/").pop() + "\`").join(", ")}`);
+ }
+ console.log();
+}
+
+// Output unique refs list
+if (refs.size > 0) {
+ console.log(`## Referenced Components (${refs.size} unique)\n`);
+ const sorted = [...refs].sort();
+ for (const r of sorted) {
+ const name = r.split("/").pop();
+ const category = r.split("/").slice(0, -1).join("/").replace("#/", "");
+ console.log(`- \`${name}\` (${category})`);
+ }
+}
+SCRIPT
diff --git a/.agents/skills/clerk-backend-api/scripts/extract-tags.js b/.agents/skills/clerk-backend-api/scripts/extract-tags.js
new file mode 100644
index 0000000..b1a4fb2
--- /dev/null
+++ b/.agents/skills/clerk-backend-api/scripts/extract-tags.js
@@ -0,0 +1,14 @@
+let input = "";
+process.stdin.on("data", d => input += d);
+process.stdin.on("end", () => {
+ const lines = input.replace(/\r/g, "").split("\n");
+ let inTags = false;
+ for (const line of lines) {
+ if (line === "tags:") { inTags = true; continue; }
+ if (inTags && line.length > 0 && line[0] !== " ") break;
+ if (inTags) {
+ const m = line.match(/^\s{2}- name:\s*(.+)/);
+ if (m) console.log(m[1]);
+ }
+ }
+});
diff --git a/.agents/skills/clerk-cli/SKILL.md b/.agents/skills/clerk-cli/SKILL.md
new file mode 100644
index 0000000..80dc6ba
--- /dev/null
+++ b/.agents/skills/clerk-cli/SKILL.md
@@ -0,0 +1,262 @@
+---
+name: clerk-cli
+description: >-
+ Operate the Clerk CLI (`clerk` binary) for authentication, user/org/session management,
+ deploy verification, instance config, env keys, and any Clerk Backend or Platform API
+ call. Use when the user mentions Clerk management tasks, "list clerk users", "create a
+ clerk user", "update organization", "pull clerk config", "clerk env pull",
+ "clerk doctor", "clerk deploy", "clerk deploy status", "clerk api", or any ad-hoc
+ Clerk API request. Prefer the CLI over raw HTTP: it handles auth, key resolution,
+ app/instance targeting, and formatting automatically.
+license: MIT
+---
+
+# Clerk CLI
+
+The `clerk` binary is a pre-authenticated gateway to Clerk's Backend API and Platform API, plus project-level tooling (auth, linking, env pulls, instance config). When the user asks anything that touches a Clerk resource, reach for `clerk` first instead of hand-rolling `curl`.
+
+> This skill targets clerk `latest`. If `clerk --version` disagrees with the latest available CLI, refresh it with `clerk skill install` or a package runner such as `bunx clerk@latest`. The binary is always the source of truth, so run `clerk --help` to verify anything this skill claims.
+
+## Execution environment (prefer the host, understand the sandbox warning)
+
+Most AI coding agents default to running shell commands in a sandbox where the
+user's home directory, OS keychain, browser launch, localhost callback
+binding, or network access may be blocked. The Clerk CLI depends on all of
+those host capabilities:
+
+- **OS credential store**: `clerk auth login` stores the OAuth token in the
+ system keychain. A sandbox without keychain access reports "not logged in"
+ even when the host is authenticated.
+- **Home-directory Clerk state**: saved config, cached metadata, and fallback
+ credentials live under the user's Clerk config/data directories.
+- **Linked project metadata**: resolved from the repo's git remote plus Clerk
+ config. Sandboxes with stripped repo state or blocked home-dir reads can
+ misreport "not linked".
+- **Local `.env*` files**: publishable and secret keys materialized by
+ `clerk env pull`.
+- **Outbound network access to Clerk**: every Backend and Platform API call.
+- **Browser + localhost OAuth callback**: `clerk auth login` needs both.
+
+In agent mode, the CLI now does a **best-effort warn-once check** at the
+host-sensitive library boundaries. When it detects that host-only Clerk state
+or system capabilities are unavailable, it emits:
+
+```text
+Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run.
+Re-run this command on the host shell before trusting auth, link, env, or API failures.
+```
+
+Treat that warning as authoritative. The command may continue, but any auth,
+link, env, config, API, browser, or OAuth callback failure from that
+invocation is untrusted until you rerun the same command on the host.
+
+**Prefer these commands on the user's host shell, not in a sandbox:**
+
+`clerk doctor`, `clerk whoami`, `clerk auth login`, `clerk link`, `clerk env pull`,
+`clerk apps ...`, `clerk config ...`, `clerk api ...`.
+
+If a command was accidentally run in a sandbox and it reports `Not logged in`,
+`auth_required`, `not linked`, missing env, keychain/file permission errors,
+or network failures, **do not treat the result as authoritative**. Rerun it on
+the host before acting on it or reporting it to the user.
+
+## Invoking the CLI
+
+Before running any `clerk` command, figure out which binary to invoke and bind that choice for the rest of the session:
+
+```sh
+# 1. Prefer a globally installed binary when it matches the skill's target version.
+command -v clerk >/dev/null 2>&1 && clerk --version
+```
+
+If that prints `latest` or any version you trust, use bare `clerk` for the rest of the session.
+
+Otherwise fall back to a package runner, in this order (matches the CLI's own `preferredRunner` logic, which prefers the runner that matches the project's lockfile):
+
+| Project package manager | Invocation |
+| ------------------------- | -------------------------------- |
+| bun (`bun.lock*`) | `bunx clerk@latest` |
+| npm (`package-lock.json`) | `npx -y clerk@latest` |
+| pnpm (`pnpm-lock.yaml`) | `pnpm dlx clerk@latest` |
+| yarn >= 2 (`yarn.lock`) | `yarn dlx clerk@latest` |
+
+Yarn Classic (v1) has no `dlx`; treat those projects as "no preferred runner" and fall back to the first runner from the list above that's on PATH.
+
+The published npm package is **`clerk`**, not `@clerk/cli`. Never teach `npm install -g clerk` as the primary path. If the global CLI is stale or behaves differently from this skill, either upgrade the global install or fall back to the `latest` runner form above.
+
+## Prerequisites (run at session start)
+
+Before running any other Clerk command in a session, verify the CLI is authenticated, linked, and healthy:
+
+```sh
+clerk --version # confirm the binary is on PATH
+clerk doctor --json # structured health check; exit 1 if anything failed
+```
+
+**Always run `clerk doctor --json` first.** It catches the common setup failures (not logged in, project not linked, missing keys, stale CLI version) up front, so later commands don't fail with confusing errors. In agent mode it also includes a `Host execution` check that warns when Clerk's host-side config / credential directories are not writable, which is the canonical signal that the current invocation is likely sandboxed.
+
+Each result has `name`, `status` (`pass`/`warn`/`fail`), `message`, optional `detail`, optional `remedy` (how to fix it), and optional `fix` (label for auto-fixable issues). Parse that and act on it, or surface it to the user. If `Host execution` warns, rerun the command on the host before trusting any auth/link/env/API failures from the same sandboxed run. Rerun `clerk doctor --json` whenever a later command starts misbehaving.
+
+If `clerk --version` reports a newer CLI than this skill covers, trust `clerk --help` first and refresh this skill bundle from its source.
+
+## The mental model
+
+| Layer | What it does | Commands |
+| ------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
+| **Session / project** | Auth, link a repo to a Clerk app, pull env keys | `auth login`, `link`, `unlink`, `whoami`, `env pull`, `doctor` |
+| **Instance config** | Manage the configuration (social providers, session lifetimes, etc.) for a specific instance | `config pull`, `config schema`, `config patch`, `config put` |
+| **Backend API (default)** | Runtime data: users, orgs, sessions, invitations, JWT templates, webhooks | `clerk api ` |
+| **Platform API (`--platform`)** | Account-level: applications, instances, billing | `clerk api --platform ` |
+
+A project is "linked" to an application via `clerk link`. Once linked, most commands auto-resolve the target app and dev instance from the repo's git remote. To target something else, pass `--app ` and/or `--instance dev|prod|`. See [references/auth.md](references/auth.md) for the full resolution order.
+
+## Discover endpoints - don't memorize them
+
+The CLI ships with the Clerk OpenAPI catalog. Always discover endpoints dynamically instead of guessing paths:
+
+```sh
+clerk api ls # list every Backend API endpoint
+clerk api ls users # filter by keyword (matches path, summary, tag, operationId)
+clerk api ls --platform apps # list Platform API endpoints
+```
+
+Use this before `clerk api `. If you don't see the endpoint you expected, it probably isn't exposed.
+
+## The `clerk api` command (the workhorse)
+
+`clerk api` makes authenticated HTTP calls. It auto-resolves keys, auto-detects method from body presence, supports stdin, and can preview mutations with `--dry-run`.
+
+```sh
+# GET requests
+clerk api /users # list users
+clerk api /users/user_abc123 # fetch one
+clerk api /users?limit=5&order_by=-created_at # query params work inline
+
+# Mutating requests
+clerk api /users -d '{"email_address":["a@b.co"]}' # POST (auto-detected from body)
+clerk api /users/user_abc123 -X PATCH -d '{"first_name":"A"}'
+clerk api /users/user_abc123 -X DELETE
+
+# Body from file or stdin
+clerk api /users --file payload.json
+cat payload.json | clerk api /users
+
+# Always preview mutations first
+clerk api /users/user_abc123 -X DELETE --dry-run
+clerk api /users/user_abc123 -X DELETE --yes # skip confirmation once you've verified
+
+# Target a specific app/instance
+clerk api /users --app app_abc123 --instance prod
+
+# Include response headers when debugging
+clerk api /users --include
+
+# Platform API (account-level, not tenant data)
+clerk api /v1/platform/applications --platform
+```
+
+For instance config, prefer the dedicated `clerk config ...` commands over raw Platform API `/config` paths. They handle dry-run, diffing, and confirmation more cleanly than the raw endpoint form.
+
+**Always `--dry-run` a mutation before running it for real.** Then re-run without `--dry-run` (add `--yes` if you're sure). In agent mode, interactive confirmation is bypassed, so `--dry-run` is the only safety net for destructive calls.
+
+**JSON bodies must be valid JSON.** The CLI validates and rejects malformed payloads.
+
+**Endpoint paths may be given with or without `/v1/` prefix** - both work for Backend API calls. The CLI normalizes.
+
+See [references/recipes.md](references/recipes.md) for concrete patterns: listing/filtering users, creating orgs, impersonation sessions, etc.
+
+## Inspecting large outputs (do not flood your context)
+
+`users list`, `apps list`, `config pull`, and most `clerk api` GETs return payloads that can be many kilobytes or megabytes. Production tenants commonly have thousands of users; an instance config can be hundreds of fields deep. Reading those responses into the conversation costs context window for no benefit. Save the response to a file first, then query just what you need with `jq`:
+
+```sh
+# 1. Persist the response. Use --limit 250 to maximize page size for users list.
+clerk users list --json --limit 250 > /tmp/users.json
+clerk apps list --json > /tmp/apps.json
+clerk api /users/user_abc123 > /tmp/user.json
+
+# 2. Inspect only what you need.
+jq '.data | length' /tmp/users.json # current page size
+jq '.hasMore' /tmp/users.json # are more pages available?
+jq '.data[0] | keys' /tmp/users.json # discover the user shape once
+jq '.data[] | {id, email_addresses}' /tmp/users.json # project to a few fields
+jq '[.data[] | select(.banned)] | length' /tmp/users.json # aggregate without reading rows
+```
+
+**If `jq` is not available**, fall back to Python or Node - both can stream the file without printing it whole:
+
+```sh
+python3 -c 'import json; d=json.load(open("/tmp/users.json")); print(len(d["data"]), d["hasMore"])'
+node -e 'const d=require("/tmp/users.json"); console.log(d.data.length, d.hasMore)'
+```
+
+`cat` / `head` the file only when you genuinely need to see the raw structure for one-off debugging. When walking pages, write each page to its own file (e.g. `page-${offset}.json`) so individual pages stay independently inspectable.
+
+## Core commands at a glance
+
+| Command | Purpose | Key flags |
+| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `clerk init` | Scaffold Clerk into a project. `--starter` only supports bootstrap for Next.js, React Router, Astro, Nuxt, TanStack Start, React, Vue, and JavaScript. | `--framework`, `--pm`, `--name` (with `--starter`), `--app`, `--starter`, `-y`, `--no-skills` |
+| `clerk auth login` | OAuth browser login (stores token). Agent mode: no-op if already logged in. With no stored session it still opens a browser and binds a localhost callback, so it is not unattended; prefer `CLERK_PLATFORM_API_KEY` for headless flows. Aliases: `signup`, `signin`, `sign-in`. Top-level shortcut: `clerk login`. | - |
+| `clerk auth logout` | Clear stored credentials. Aliases: `signout`, `sign-out`. Top-level shortcut: `clerk logout`. | - |
+| `clerk whoami` | Print the logged-in email. | - |
+| `clerk link` / `clerk unlink` | Link this repo to a Clerk app, or remove the link. `unlink` requires `--yes` in agent mode. | (see `--help`) |
+| `clerk env pull` | Write publishable + secret keys to the framework's env file (merge, not clobber). Resolves `.env.development.local` → framework-preferred file → `.env.local`; override with `--file`. | (see `--help`) |
+| `clerk config {pull,schema}` | Fetch instance config JSON, or its JSON Schema. | (see `--help`) |
+| `clerk config patch` | Partial update (PATCH) of instance config. Pass `--destructive` to actually delete sub-resources touched by the patch rather than resetting them to defaults. | `--app`, `--instance`, `--file`, `--json`, `--dry-run`, `--yes`, `--destructive` |
+| `clerk config put` | Full replacement (PUT) of instance config. Pass `--destructive` to actually delete removed sub-resources rather than resetting them to defaults. | `--app`, `--instance`, `--file`, `--json`, `--dry-run`, `--yes`, `--destructive` |
+| `clerk apps {list,create}` | List or create Clerk applications. Defaults to JSON in agent mode. | (see `--help`) |
+| `clerk users` (no subcommand) | Interactive picker for `users` actions in human mode; in agent mode prints the action list and exits `2`. Always pass an explicit subcommand from agents. | `--app`, `--instance`, `--secret-key` |
+| `clerk users list` | List users via curated BAPI flags. JSON output (default when piped or in agent mode) is `{data, hasMore}` so callers can paginate without `/users/count`. `--limit` defaults to 100 (max 250). | `--limit`, `--offset`, `--query`, `--email-address`, `--phone-number`, `--username`, `--user-id`, `--external-id`, `--order-by`, `--json`, `--app`, `--instance`, `--secret-key` |
+| `clerk users create` | Create a user from curated flags or a raw BAPI body. Confirmation prompt unless `--yes`. | `--email`, `--phone`, `--username`, `--password`, `--first-name`, `--last-name`, `--external-id`, `-d, --data`, `--file`, `--dry-run`, `--yes`, `--json` |
+| `clerk users open [user-id]` | Open a user's dashboard page. Agent mode requires `user-id` and prints a JSON descriptor instead of launching a browser. | (see `--help`) |
+| `clerk open [subpath]` | Open the linked app's dashboard in a browser. Agent mode: prints a JSON descriptor instead of opening. | (see `--help`) |
+| `clerk deploy` | Human-mode production deploy wizard. Agent mode: emits a read-only JSON handoff and tells the agent whether to ask the human to run the wizard, wait for provisioning, finish OAuth, or do nothing. | `--mode agent`, `--mode human`, `--verbose` |
+| `clerk deploy status` | Read-only deploy verification. Triggers a DNS check, reports aggregate domain and OAuth readiness, and exits `0` only when complete. Agent mode does one quick check by default; pass `--wait` to keep waiting. | `--mode agent`, `--wait`, `--verbose` |
+| `clerk doctor` | Health check (CLI version, login, link, env, config, completion; plus host-execution probe in agent mode). | `--json`, `--spotlight`, `--verbose`, `--fix` |
+| `clerk api [path]` | Authenticated HTTP to Backend/Platform API. | `-X`, `-d`, `--file`, `--dry-run`, `--yes`, `--include`, `--app`, `--secret-key`, `--instance`, `--platform` |
+| `clerk api ls [filter]` | Discover endpoints from the bundled OpenAPI catalog. | (see `--help`) |
+| `clerk completion [shell]` | Print a shell completion script (`bash`, `zsh`, `fish`, `powershell`). | - |
+| `clerk update` | Update the CLI to the latest version. | `--channel`, `-y`, `--all` |
+| `clerk skill install` | Reinstall the bundled `clerk-cli` skill from the CLI. In this standalone bundle, update the skill source directly instead. | (see `--help`) |
+
+**`clerk --help` is the source of truth for flags.** This table is a hint, not a spec. Before running an unfamiliar command or flag combination, run `clerk --help` once per session. Every command also defines `setExamples([...])` in source, which `--help` renders as a copy-pasteable Examples block, so you rarely need to guess syntax.
+
+## Agent-mode behavior (important)
+
+The CLI auto-detects agent mode when stdout is not a TTY, or when `--mode agent` / `CLERK_MODE=agent` is set. In agent mode:
+
+- **Interactive prompts are disabled.** Commands that would normally show pickers (`link` without `--app`, `unlink` without `--yes`, `users` without a subcommand) either auto-resolve or exit with a usage error. `clerk api` with no args prints usage guidance and exits 0; pass an endpoint (or `ls`) explicitly. Always pass explicit flags (`--app`, `--yes`) in scripted calls.
+- **Host-sensitive operations emit a sandbox warning once per invocation.** Home-directory Clerk state, keychain access, networked Clerk calls, browser launch, and localhost OAuth callback setup can trigger the warning shown above. If it appears, rerun the same command on the host before trusting the result.
+- **If your harness does not clearly present as agent mode, force it.** Use `--mode agent` or `CLERK_MODE=agent` when you want the CLI's non-interactive behavior and sandbox warning path to apply deterministically.
+- **`link` supports deterministic agent flows.** In agent mode, `clerk link --app ` links directly. Without `--app`, the CLI will try silent key-based autolink first; if it cannot determine the app unambiguously, it exits and tells you to pass `--app`.
+- **`init` never selects or creates a real Clerk app for you in agent mode unless authenticated or given a target.** Pass `--app ` (or pre-link the project) to authenticate and link a real app, or pass `--keyless` to use auto-generated temporary development keys when bootstrapping a new project on a keyless-capable framework. Without either, agent mode prints manual setup guidance and exits cleanly.
+- **`unlink` requires `--yes` in agent mode.** This preserves the same safety bar as other destructive commands while still letting an agent complete the unlink non-interactively.
+- **Mutations still require `--yes`** unless you accept per-call confirmation is impossible.
+- **`doctor --fix` is ignored.** Parse `doctor --json` output's `remedy` field and act on it yourself.
+- **`apps list` and `apps create` default to JSON** when piped.
+- **`users` defaults to JSON when piped, like `apps`.** `clerk users list` and `clerk users create` emit JSON in agent mode. Bare `clerk users` (no subcommand) is a usage error in agent mode - pass `list`, `create`, or `open` explicitly. `clerk users open` requires the `user-id` positional in agent mode and prints a JSON descriptor instead of launching a browser.
+- **`deploy` has an agent handoff plus a verification gate.** In agent mode, bare `clerk deploy` is read-only and emits a JSON handoff. It never drives the interactive wizard. Do not tell Claude or another agent to run `! clerk deploy`, because the wizard needs interactive stdin prompts. Ask the human to run `clerk deploy` in a new terminal window when needed, then run `clerk deploy status --mode agent` to verify completion. See [references/agent-mode.md](references/agent-mode.md#deploy-handoff-and-verification).
+- **`--input-json `** expands JSON into flags on any command (e.g. `clerk init --input-json '{"framework":"next","yes":true}'`). Piped stdin is also accepted: `echo '{"yes":true}' | clerk init`. Place `--input-json` after the leaf subcommand. Full rules in [references/agent-mode.md](references/agent-mode.md#passing-options-as-json---input-json).
+
+Full matrix and sandbox details in [references/agent-mode.md](references/agent-mode.md).
+
+## Output format and errors
+
+- **JSON output:** `--json` on `apps list` and `doctor`. For `clerk api`, the response body is the raw API JSON, so pipe into `jq` freely.
+- **Exit codes:** `0` success, `1` runtime error, `2` usage/validation error. `doctor` returns `1` if any check failed.
+- **Error format:** User-facing errors print a single line to stderr and set a non-zero exit code. Use `--verbose` for stack traces when debugging.
+
+## Safety rules for autonomous use
+
+1. **Discover before acting:** `clerk api ls ` before `clerk api `.
+2. **Preview mutations:** `--dry-run` on every `config patch`, `config put`, `api -X POST/PATCH/PUT/DELETE`.
+3. **Target explicitly in production:** pass `--instance prod` rather than relying on defaults, and confirm with the user before any production mutation.
+4. **Never commit secrets:** `env pull` writes to `.env.local` (which should be gitignored). Don't paste secret keys into code or chat.
+5. **Use `doctor --json`** to diagnose before assuming the CLI is broken.
+
+## References
+
+- [references/auth.md](references/auth.md) - auth flow, key resolution order, host-vs-sandbox behavior, `--app`/`--instance` targeting, Backend vs Platform API.
+- [references/recipes.md](references/recipes.md) - copy-pasteable recipes for common Clerk tasks.
+- [references/agent-mode.md](references/agent-mode.md) - agent-mode behavior matrix, sandbox warning semantics, exit codes, error format.
diff --git a/.agents/skills/clerk-cli/references/agent-mode.md b/.agents/skills/clerk-cli/references/agent-mode.md
new file mode 100644
index 0000000..7ed4c88
--- /dev/null
+++ b/.agents/skills/clerk-cli/references/agent-mode.md
@@ -0,0 +1,287 @@
+# Clerk CLI - Agent Mode Reference
+
+The Clerk CLI has a first-class "agent" mode that's designed for non-interactive and AI-driven use. Read this before writing scripts or letting an LLM drive the CLI.
+
+## Sandbox warning semantics
+
+Agent mode and sandboxing are related but not identical:
+
+- **Agent mode** controls non-interactive behavior.
+- **Sandboxing** controls whether the CLI can actually reach host-only Clerk
+ state and host system capabilities.
+
+In agent mode, the CLI now performs a **best-effort warn-once check** at the
+host-sensitive integration boundaries. The first time an invocation hits a
+blocked host capability, it emits:
+
+```text
+Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run.
+Re-run this command on the host shell before trusting auth, link, env, or API failures.
+```
+
+Treat that warning as authoritative. The command may still continue and return
+an ordinary Clerk error, but any auth/link/env/config/API/browser/OAuth
+failure from that invocation should be treated as suspect until rerun on the
+host.
+
+The warning can be triggered by:
+
+- home-directory Clerk config / credential file access
+- OS keychain access
+- outbound Clerk network requests
+- browser launch
+- localhost callback server binding for OAuth
+
+If your harness does not obviously look non-interactive, force agent behavior
+with `--mode agent` or `CLERK_MODE=agent` so the CLI's non-interactive and
+sandbox-warning paths apply deterministically.
+
+## How agent mode is detected
+
+Priority (first match wins):
+
+1. `--mode agent` flag on the command line
+2. `CLERK_MODE=agent` environment variable
+3. Stdout is not a TTY (piped, redirected, or running under an agent harness)
+
+Force human mode with `--mode human` or `CLERK_MODE=human`. Typical AI-agent invocations automatically land in agent mode because stdout is piped.
+
+## What changes in agent mode
+
+| Behavior | Human mode | Agent mode |
+| ---------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Interactive pickers (`link` without `--app`, `api` with no args) | Show a TUI picker | Print structured guidance and exit, or auto-resolve |
+| `clerk link --app ` | Links directly | Links directly |
+| `clerk link` without `--app` | Interactive picker / create UI | Tries silent autolink from detected publishable keys; if no deterministic match exists, exits with a usage error telling the caller to pass `--app` |
+| Confirmation prompts (`unlink`, `config patch`, `api -X DELETE`) | Prompt y/n | Require `--yes`, otherwise error |
+| `clerk doctor --fix` | Interactively offers fixes | **Ignored**; output the `remedy` field and let the caller act |
+| `clerk apps list` default output | Table | JSON (when piped) |
+| `clerk apps create ` output | Human-readable summary | JSON (auto-detected, same as `apps list`); `--json` also works explicitly |
+| `clerk users list` / `clerk users create` default output | Table / human-readable | JSON (auto-detected when piped); `--json` also works explicitly |
+| `clerk users` (no subcommand) | Interactive action picker | Prints the action list and exits with a usage error (code `2`) - pass `list` / `create` / `open` |
+| `clerk users open [user-id]` | Picks a user interactively, opens browser | Requires `user-id`; prints `{url, appId, appName, instanceId, instanceLabel, userId, opened: false}` and does not open a browser |
+| `clerk open [subpath]` | Opens the browser to the URL | Does not open a browser. Prints a JSON descriptor (`{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}`) on stdout so the agent can surface it |
+| `clerk deploy` | Interactive production deploy wizard | Read-only handoff. Emits deploy status JSON on stdout and exits `0` for linked projects. Does not prompt, mutate, trigger DNS checks, or poll. |
+| `clerk deploy status` | Verify production deploy state | Read-only verification gate. Triggers one DNS check for active production domains, waits briefly, reads one live status snapshot, emits status JSON on stdout, exits `0` when complete and `1` when incomplete. It does not keep waiting or back off in agent mode unless `--wait` is passed. Use `--wait` when the user asks the agent to keep waiting for DNS, SSL, email DNS, or final Clerk-side readiness. |
+| `clerk auth login` when already authenticated | Prompt to re-auth | Silent no-op |
+| `clerk init` | Full interactive scaffold flow | Runs non-interactively. Explicit `--app` or a linked profile uses the real-app auth/link/env flow; without it, an authenticated agent on a keyless-capable framework creates a real app and links it. Pass `--keyless` to opt into auto-generated dev keys for new projects on keyless-capable frameworks. Without `--keyless`, an unauthenticated agent (or non-keyless framework with no app target) prints manual setup guidance. |
+| Color / spinners | Enabled | Disabled |
+
+In addition, sandboxed agent-mode invocations may emit the warning above once
+per CLI invocation when a host-sensitive operation is blocked.
+
+**Rule of thumb:** always pass `--yes` for mutations and `--json` for structured output where available. Pass `--app` / `--instance` when you intentionally target a real app; pass `--keyless` to opt into auto-generated dev keys when bootstrapping a new project without authenticating.
+
+## Passing options as JSON: `--input-json`
+
+Every command accepts `--input-json `. Keys convert from camelCase/snake_case to kebab-case and expand into flags before Commander parses argv - so anything a command accepts as a flag can come from JSON instead.
+
+```sh
+clerk init --input-json '{"framework":"next","yes":true}'
+clerk config pull --input-json '{"keys":["auth_email","session"]}' # arrays → repeated flags
+clerk init --input-json @init-opts.json # read JSON from a file
+clerk init --input-json - # read JSON from stdin
+echo '{"framework":"next","yes":true}' | clerk init # auto-detect piped stdin
+```
+
+When `--input-json` is omitted and stdin is piped (not a TTY), the CLI automatically reads JSON from stdin - no flag needed. This lets agents pipe options directly: `echo '{"yes":true}' | clerk init`.
+
+Positional arguments (e.g. the `` in `clerk apps create `) cannot come from JSON - only flag-style options can.
+
+| JSON | Expansion |
+| ---------------- | --------------------------------------- |
+| `"str"` / number | `--flag ` |
+| `true` | `--flag` |
+| `false` / `null` | omitted |
+| `["a","b"]` | `--flag a --flag b` (empty arrays omit) |
+| `{…}` (nested) | rejected - `invalid_json`, exit `2` |
+
+**Placement.** Put `--input-json` after the leaf subcommand. Before it, flags land on the root program, so only `--mode` / `--verbose` work there - subcommand flags (`--json`, `--app`, etc.) error as unknown. Explicit flags after `--input-json` override its values (last-flag-wins).
+
+Errors use the standard agent-mode format: bad JSON → `invalid_json`, missing `@file` → `file_not_found`, unknown expanded flags → Commander's `unknown option`. All exit `2`.
+
+## Exit codes
+
+| Code | Meaning |
+| ---- | ---------------------------------------------------------------------------- |
+| `0` | Success |
+| `1` | Runtime error (auth failure, API error, file I/O, etc.) |
+| `2` | Usage or validation error (bad flags, malformed JSON body, unknown endpoint) |
+
+`clerk doctor` exits `1` when any check fails (warnings alone still exit `0`).
+
+## Error output format
+
+**Human mode:**
+
+- Single-line error message on stderr.
+- Stack traces hidden unless `--verbose` is passed.
+- API errors include the first message from the response body, prefixed with a human context string (e.g., `Failed to fetch config: unauthorized`).
+
+**Agent mode:**
+
+- Structured JSON on stderr: `{"error":{"code":"...","message":"...","docsUrl?":"...","errors?":[...]}}`.
+- `code` is a machine-readable error code (e.g., `auth_required`, `api_error`, `unexpected_error`).
+- `errors` array is present for API errors and mirrors the Clerk API error shape (`{code?, message?, meta?}`).
+- `docsUrl` is present when the error has associated documentation.
+
+**Both modes:**
+
+- User-aborted commands exit cleanly with no error output.
+- When handling errors programmatically, read stderr, check the exit code, and re-run with `--verbose` to get a trace if you need to debug.
+
+## Structured outputs you can rely on
+
+| Command | Structured output |
+| --------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| `clerk doctor --json` | `[{name, status, message, detail?, remedy?, fix?}]` |
+| `clerk apps list --json` | Array of application objects |
+| `clerk apps create --json` | Single application object |
+| `clerk users list` (agent mode or `--json`) | `{data: [...users], hasMore}` envelope (BAPI user shape inside `data`) |
+| `clerk users create` (agent mode or `--json`) | Single user object (raw BAPI shape) |
+| `clerk users open` (agent mode) | `{url, appId, appName, instanceId, instanceLabel, userId, opened: false}` |
+| `clerk api ` | Raw API JSON (Backend or Platform) on stdout |
+| `clerk api --include` | Response headers on stderr, body on stdout |
+| `clerk config pull` | Instance config JSON |
+| `clerk config schema` | JSON Schema |
+| `clerk open [subpath]` | `{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}` (agent mode) |
+| `clerk open --print` | Plain dashboard URL on stdout |
+| `clerk deploy` (agent mode) | Deploy handoff report with `complete`, `state`, domain status, OAuth status, and `nextAction` |
+| `clerk deploy status` (agent mode) | Deploy verification report with the same shape, plus exit `0` complete or `1` incomplete |
+| Any command (agent mode) | On error: `{"error":{"code","message","docsUrl?","errors?"}}` on stderr |
+
+For commands without an explicit `--json` flag, `clerk api` is your escape hatch: hit the underlying endpoint directly.
+
+## Patterns for agent-driven use
+
+### Diagnose before acting
+
+```sh
+clerk doctor --json --spotlight
+```
+
+Parse the output, then for each failing check read `remedy` and act. Never call `--fix` from an agent - it's interactive.
+
+In agent mode, `doctor` also includes a **`Host execution`** check when it can
+detect that Clerk's host-side state is not writable. If that check warns, stop
+trusting auth/link/env/API failures from the same sandboxed run and rerun the
+relevant command on the host.
+
+### Preview every mutation
+
+```sh
+# Dry run first
+clerk api /users/user_abc123 -X DELETE --dry-run
+# If the preview is what you expected, run it with --yes
+clerk api /users/user_abc123 -X DELETE --yes
+```
+
+### Target explicitly
+
+```sh
+# Don't rely on the linked profile for critical operations
+clerk api /users --app app_abc123 --instance prod
+```
+
+The same advice applies to linking in agent mode: `clerk link --app app_abc123` is deterministic and works non-interactively. If you omit `--app`, the command only succeeds when silent autolink can prove the target app from existing publishable keys.
+
+### Deploy handoff and verification
+
+Do not try to drive the interactive deploy wizard from an agent. Use the handoff and check commands instead.
+
+```sh
+# 1. Inspect current production deploy state without mutating anything.
+clerk deploy --mode agent
+
+# 2. If the handoff says a human action is needed, ask the user to run this
+# in a new terminal window, not through `! clerk deploy`:
+clerk deploy --mode human
+
+# 3. After the user finishes or DNS has had time to propagate, verify:
+clerk deploy status --mode agent
+
+# 4. If the user asks you to keep waiting, use the retrying wait loop:
+clerk deploy status --mode agent --wait
+```
+
+`clerk deploy --mode agent` is read-only. It resolves the linked app and current production deploy snapshot, then emits JSON on stdout. It does **not** trigger DNS checks, poll, create production instances, patch OAuth config, or prompt. Linked projects exit `0` because this is an informational handoff. Not-linked and API failures still use the normal agent error envelope on stderr.
+
+Never try to run the human wizard through Claude's `! clerk deploy` shell escape or any non-interactive agent shell. The deploy wizard asks for domain, DNS export, OAuth, and verification inputs over stdin, so it needs a real human terminal. Tell the user to open a new terminal window in the project directory and run `clerk deploy` or `clerk deploy --mode human` there. After they finish, return to agent mode and run `clerk deploy status --mode agent`.
+
+`clerk deploy status --mode agent` is the gate. It is also read-only with respect to deploy configuration, but for an active production domain it triggers one Clerk DNS check, waits briefly, reads a live status/config snapshot, then reports DNS, SSL, email DNS, aggregate domain readiness, and OAuth completeness. By default it does not keep waiting or exponentially back off in agent mode. If the check is incomplete and the user asks the agent to continue waiting, run `clerk deploy status --mode agent --wait` instead of manually sleeping and retrying. `--wait` uses the shared poll loop: one immediate status read, then up to 5 exponential-backoff retries until aggregate domain status is complete. It emits the same status JSON. It exits:
+
+| Exit | Meaning |
+| ---- | ------------------------------------------------------------------------------------ |
+| `0` | Deploy is complete and verified. |
+| `1` | The check ran successfully, but deploy is incomplete. Read `state` and `nextAction`. |
+| else | A real CLI error occurred. Read the standard agent error envelope on stderr. |
+
+Deploy-specific agent errors still use the standard envelope and may include typed codes such as `plan_insufficient`, `provider_domain_not_allowed`, `home_url_taken`, or `form_param_invalid`.
+
+When a production instance exists, `nextAction` includes the full Clerk Dashboard domains URL so agents can send the user directly to the same page the human CLI prints in its next steps. Always show that URL to the user. Ask whether they want you to open it for them instead of omitting or paraphrasing it away.
+
+The deploy report has this shape:
+
+```json
+{
+ "complete": false,
+ "state": "domain_pending",
+ "domain": "example.com",
+ "productionInstanceId": "ins_...",
+ "domainStatus": { "dns": "complete", "ssl": "pending", "mail": "complete" },
+ "pendingDnsRecords": [{ "type": "CNAME", "host": "clerk.example.com", "value": "..." }],
+ "oauth": { "complete": true, "configured": ["google"], "pending": [], "unsupported": [] },
+ "nextAction": "SSL still provisioning for example.com. Re-run `clerk deploy status` in a few minutes, DNS propagation can take time. Ask the user to visit the Clerk Dashboard domains page, or offer to open it: https://dashboard.clerk.com/apps/app_.../instances/ins_.../domains"
+}
+```
+
+`complete` is `true` only when the aggregate domain status is complete and all supported OAuth providers enabled in development have production credentials. The `domainStatus` object is a component summary; DNS, SSL, and email DNS can all read `complete` while `state` remains `domain_pending` if Clerk-side finalization is still pending.
+
+State precedence:
+
+| State | What to do |
+| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `not_started` | Ask the human to run `clerk deploy --mode human`, then run `clerk deploy status --mode agent`. |
+| `domain_provisioning` | Wait briefly or ask the human to finish `clerk deploy`, then run `clerk deploy status --mode agent`. |
+| `domain_pending` | Surface `pendingDnsRecords` when present. Re-run `clerk deploy status --mode agent` after DNS, SSL, or email DNS propagation. |
+| `oauth_pending` | Ask the human to finish the OAuth credential steps in `clerk deploy --mode human`, then verify with `deploy status`. |
+| `complete` | No action needed. |
+
+Unsupported OAuth providers do not block `complete`, because the wizard cannot configure them automatically. They are still surfaced in `oauth.unsupported` so you can warn the user to review them in the Clerk Dashboard.
+
+### Use the catalog, not hard-coded paths
+
+```sh
+clerk api ls users # discover available user endpoints
+clerk api ls --platform apps # platform-side endpoints
+```
+
+### Surface doctor remedies to the user
+
+When `clerk doctor --json` reports a failure, show the user the `name`, `message`, and `remedy` - don't just silently try to fix it, because the underlying fix (e.g., `clerk auth login`) usually requires human interaction.
+
+`clerk doctor --fix` is disabled in agent mode, so you cannot rely on it. If a caller wants to attempt remediation anyway, map the failing check to the command that would fix it in human mode. Each check exposes this mapping via the optional `fix.label` field on the JSON result:
+
+| Failing check | Manual remediation |
+| ----------------------- | -------------------------------------------- |
+| `Logged in` | `clerk auth login` |
+| `Authentication valid` | `clerk auth login` |
+| `CLI configuration` | `clerk auth login` |
+| `Project linked` | `clerk link` |
+| `Application reachable` | `clerk link` |
+| `Instance IDs` | `clerk link` |
+| `Environment variables` | `clerk env pull` |
+| `CLI version` | (no auto-fix; run `clerk update`) |
+| `Shell completion` | (no auto-fix; see `clerk completion --help`) |
+
+All three remediation commands are themselves interactive by default: `auth login` opens a browser, `link` prompts for an app when `--app` is omitted, and `env pull` writes a file. In agent mode, prefer `clerk link --app ` over bare `clerk link`, since the bare form only works when silent autolink can resolve the target app without a picker.
+
+## What NOT to do in agent mode
+
+- **Don't ignore the sandbox warning.** If the CLI says host-only Clerk state or system capabilities may be unavailable, rerun the same command on the host before trusting the result.
+- **Don't assume `clerk auth login` is fully unattended from an agent** - it opens a browser and waits for a callback. Prefer `CLERK_PLATFORM_API_KEY` for headless automation. `clerk init --app ` or init in an already linked project may still invoke the normal login fallback when a real app target is explicit.
+- **Don't call `clerk link` without `--app` and assume the agent can pick for you** - it only succeeds when silent autolink can determine the app from detected keys.
+- **Don't run `clerk unlink` in agent mode without `--yes`** - it exits with a usage error instead of prompting.
+- **Don't run `clerk config put` without `--dry-run` first** - it's a full replacement and is destructive.
+- **Don't skip `--yes` on mutations and expect them to work** - agent mode disables prompts, so commands that require confirmation will error.
+- **Don't leak secret keys into logs** - the CLI never prints the raw secret key, and you shouldn't either.
diff --git a/.agents/skills/clerk-cli/references/auth.md b/.agents/skills/clerk-cli/references/auth.md
new file mode 100644
index 0000000..e77ed7c
--- /dev/null
+++ b/.agents/skills/clerk-cli/references/auth.md
@@ -0,0 +1,155 @@
+# Clerk CLI - Authentication & Targeting Reference
+
+Everything you need to know about how the CLI authenticates, resolves keys, and targets the right application/instance.
+
+## Two APIs, two auth paths
+
+Clerk exposes two HTTP APIs. The CLI speaks both.
+
+| API | Base URL | Auth | Used for | CLI flag |
+| ------------------------ | --------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------ |
+| **Backend API (BAPI)** | `https://api.clerk.dev/v1/` | Instance **secret key** (`sk_...`) | Tenant data: users, orgs, sessions, invitations, JWT templates, webhooks. | (default) |
+| **Platform API (PLAPI)** | `https://api.clerk.com/v1/` | **Platform API key** (`ak_...`) or OAuth token from `clerk auth login` | Account-level: listing your applications, fetching app/instance metadata, pulling config, billing. | `--platform` |
+
+You override the base URLs via `CLERK_BACKEND_API_URL` and `CLERK_PLATFORM_API_URL` when testing against non-production Clerk environments.
+
+### Backend API secret key resolution order
+
+When you run `clerk api /users` (no `--platform`), the CLI picks the `sk_` key in this order:
+
+1. `--secret-key ` flag (explicit override)
+2. `CLERK_SECRET_KEY` environment variable
+3. Auto-resolved from `--app ` (uses `CLERK_PLATFORM_API_KEY` or stored OAuth token to fetch the app's secret key)
+4. Auto-resolved from the linked project profile (same mechanism as #3, but the app ID comes from the repo's link)
+
+The CLI validates prefixes: passing `ak_...` where `sk_...` is expected (or vice versa) throws an error immediately with guidance on which key type to use.
+
+### Platform API auth resolution order
+
+When you run `clerk api --platform ...`, or any command that already uses PLAPI (`apps list`, `config pull`, `link`, etc.), the CLI picks the bearer token in this order:
+
+1. `CLERK_PLATFORM_API_KEY` environment variable
+2. Stored OAuth token from `clerk auth login`
+3. If neither is present, the CLI errors: "Not authenticated. Run `clerk auth login` or set `CLERK_PLATFORM_API_KEY`."
+
+Set `CLERK_PLATFORM_API_KEY` for CI and scripted agent usage. Use `clerk auth login` for local interactive development.
+
+## Host vs sandbox behavior
+
+These auth and targeting rules only produce trustworthy results when the CLI
+can actually reach the user's host state.
+
+In agent mode, the CLI now emits a best-effort warning once per invocation
+when it detects that host-only Clerk state or system capabilities are
+unavailable:
+
+```text
+Host-only Clerk state or system capabilities may be unavailable in agent mode. This may be a sandboxed run.
+Re-run this command on the host shell before trusting auth, link, env, or API failures.
+```
+
+That warning usually means one of the following is blocked:
+
+- Clerk home-directory config or fallback credential files
+- OS keychain access
+- outbound Clerk network access
+- browser launch or localhost OAuth callback setup
+
+When that warning appears, stop trusting the current invocation's auth or
+targeting result. A sandboxed run can misreport:
+
+- `Not logged in` / `auth_required`
+- `Not authenticated. Run clerk auth login or set CLERK_PLATFORM_API_KEY`
+- `No Clerk project linked`
+- missing env or missing linked profile state
+
+Rerun the same command on the host before acting on it.
+
+> **`config` commands do not accept `--secret-key`.** They target the Platform API and authenticate via the PLAPI chain above (`CLERK_PLATFORM_API_KEY` or the stored OAuth token). If you need to script `config pull/schema/patch/put` in CI, export `CLERK_PLATFORM_API_KEY`; a Backend API `sk_...` key will not work.
+
+## Project linking
+
+`clerk link` stores a mapping from your repo to a Clerk application in the CLI config file (run `clerk doctor --verbose` to see the resolved path; override with `CLERK_CONFIG_DIR`). The key is the normalized git remote URL (e.g., `github.com/org/repo`), which means the link is shared across all clones and worktrees of the same repo automatically.
+
+When you run a command without `--app`/`--instance`:
+
+1. The CLI resolves the current repo's profile (normalized git remote → git common dir → current working directory).
+2. If linked, it uses the stored app ID and instance IDs.
+3. If not linked, it errors with guidance to run `clerk link`.
+
+## `--app` and `--instance` targeting
+
+Most commands accept `--app ` and `--instance ` to override the linked profile:
+
+- `--app ` - Clerk application ID (starts with `app_`). Works from any directory; no link required.
+- `--instance ` - One of:
+ - `dev` (development instance, the default)
+ - `prod` (production instance)
+ - a full instance ID (starts with `ins_`)
+
+Examples:
+
+```sh
+# Operate on a specific app without linking the repo
+clerk api /users --app app_abc123
+
+# Pull production env keys (dangerous - only when you know what you're doing)
+clerk env pull --app app_abc123 --instance prod
+
+# Target a specific instance directly
+clerk config pull --instance ins_2aB3c...
+```
+
+## Auth commands
+
+### `clerk auth login`
+
+Aliases: `signup`, `signin`, `sign-in`. Top-level shortcut: `clerk login`.
+
+OAuth 2.0 PKCE flow against the Clerk OAuth system instance (`https://clerk.clerk.com` by default, overridable via `CLERK_OAUTH_BASE_URL`):
+
+1. Generates PKCE parameters.
+2. Starts a local callback server on `127.0.0.1`.
+3. Opens the browser to `/oauth/authorize`.
+4. Exchanges the code at `/oauth/token` for an access token.
+5. Fetches user info from `/oauth/userinfo`.
+6. Stores the token in the OS credential store.
+
+In agent mode, if already authenticated, it's a no-op. If not, it prints guidance rather than opening a browser.
+In a sandbox, even the "already authenticated" check can be false if the
+keychain or fallback credential file is blocked, so rerun on the host before
+trusting a sandboxed auth failure.
+
+### `clerk auth logout`
+
+Aliases: `signout`, `sign-out`. Top-level shortcut: `clerk logout`.
+
+Clears the stored token. No API calls.
+
+### `clerk whoami`
+
+Hits `GET /oauth/userinfo` with the stored token and prints the email. Exits with a message if not logged in.
+
+## Environment variables the CLI honors
+
+| Variable | Effect |
+| ------------------------ | --------------------------------------------------------------- |
+| `CLERK_MODE` | Force `human` or `agent` mode (overrides TTY detection). |
+| `CLERK_SECRET_KEY` | BAPI secret key (bypasses linked project / `--app` resolution). |
+| `CLERK_PLATFORM_API_KEY` | PLAPI bearer key. |
+| `CLERK_BACKEND_API_URL` | Override Backend API base URL. |
+| `CLERK_PLATFORM_API_URL` | Override Platform API base URL. |
+| `CLERK_OAUTH_BASE_URL` | Override OAuth base URL (advanced / internal). |
+| `CLERK_CONFIG_DIR` | Override config, cache, and credential directory (advanced). |
+
+## Common auth failure modes
+
+| Symptom | Likely cause | Fix |
+| ----------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
+| `Not authenticated` | No token stored, no `CLERK_PLATFORM_API_KEY` | `clerk auth login` or export `CLERK_PLATFORM_API_KEY` |
+| `No Clerk project linked` | Running a command that needs a linked profile with no `--app` | `clerk link` or pass `--app ` |
+| `Invalid secret key prefix` | Passed `ak_...` where `sk_...` expected (or vice versa) | Check which API the command hits; pass the matching key type |
+| `Unauthorized` from API | Key belongs to a different instance | Verify `--instance` and ensure the key matches |
+| Sandbox warning + auth/link failure | Host-only Clerk state or system capabilities are blocked | Rerun the same command on the host before trusting the error |
+
+When in doubt: `clerk doctor --json` walks through all of this and tells you exactly what's wrong.
diff --git a/.agents/skills/clerk-cli/references/recipes.md b/.agents/skills/clerk-cli/references/recipes.md
new file mode 100644
index 0000000..0ff4620
--- /dev/null
+++ b/.agents/skills/clerk-cli/references/recipes.md
@@ -0,0 +1,299 @@
+# Clerk CLI - Recipes
+
+Copy-pasteable patterns for common tasks. Treat these as starting points; confirm exact paths and parameters with `clerk api ls ` and `clerk --help`, since the Clerk API evolves.
+
+## Discovery first
+
+```sh
+clerk api ls # everything Backend API exposes
+clerk api ls users # filter by keyword
+clerk api ls --platform # Platform API (account-level)
+```
+
+The bundled catalog is cached locally for 1 hour. There is no force-refresh flag - once the TTL expires the next `clerk api ls` re-fetches automatically; on fetch failure the CLI falls back to the stale cache and prints a warning.
+
+## Users
+
+```sh
+# List users (preferred; curated flags). --limit defaults to 100 (max 250).
+# JSON output is `{ data: [...], hasMore }` so callers can paginate without /users/count.
+clerk users list
+clerk users list --limit 50 --offset 0 --order-by -created_at
+
+# Count users (no curated subcommand; use the raw API)
+clerk api /users/count
+
+# Fetch a user (no curated subcommand; use the raw API)
+clerk api /users/user_abc123
+
+# Search by email
+clerk users list --email-address alice@example.com
+
+# Open a user's profile in the dashboard
+clerk users open user_abc123
+clerk users open user_abc123 --print # print the URL instead of opening
+
+# Create a user (preferred; curated flags)
+clerk users create \
+ --email alice@example.com \
+ --password 'SuperSecret123!' \
+ --first-name Alice \
+ --last-name Doe \
+ --yes
+
+# Equivalent raw BAPI call. Use only when curated flags don't cover a field.
+clerk api /users -d '{
+ "email_address": ["alice@example.com"],
+ "password": "SuperSecret123!",
+ "first_name": "Alice",
+ "last_name": "Doe"
+}'
+
+# Update (PATCH merges)
+clerk api /users/user_abc123 -X PATCH -d '{"first_name":"Alicia"}'
+
+# Ban / unban
+clerk api /users/user_abc123/ban -X POST
+clerk api /users/user_abc123/unban -X POST
+
+# Lock / unlock
+clerk api /users/user_abc123/lock -X POST
+clerk api /users/user_abc123/unlock -X POST
+
+# Delete (PREVIEW FIRST)
+clerk api /users/user_abc123 -X DELETE --dry-run
+clerk api /users/user_abc123 -X DELETE --yes
+```
+
+### Test users (development only)
+
+For test accounts you need to sign into without real email or SMS delivery, Clerk provides two magic patterns that both verify with the fixed OTP `424242`. Use them on development instances; production rejects them.
+
+**By email.** Any address with the `+clerk_test` subaddress is recognized as a test email. The domain portion is arbitrary.
+
+```sh
+# Create a test user with a test email (dev instance)
+# `skip_password_checks` isn't a curated flag, so pass the body via `-d`.
+clerk users create -d '{
+ "email_address": ["demo+clerk_test@example.com"],
+ "password": "TestPass123!",
+ "skip_password_checks": true
+}' --yes
+```
+
+**By phone.** Any US fictional phone number in the `+1 (XXX) 555-0100` through `+1 (XXX) 555-0199` range is recognized as a test phone. Pass the E.164 form.
+
+```sh
+# Create a test user with a test phone (dev instance)
+clerk users create -d '{
+ "phone_number": ["+12015550100"],
+ "password": "TestPass123!",
+ "skip_password_checks": true
+}' --yes
+```
+
+When signing in as either user in a browser or Playwright, enter `424242` at the OTP prompt.
+
+These patterns only apply to development instances. In production, client trust blocks sign-in regardless of suffix or number, and using real-looking test addresses is highly discouraged. Test addresses and numbers do not count against the dev-instance monthly caps (20 SMS, 100 emails). See [Clerk's test emails and phones reference](https://clerk.com/docs/guides/development/testing/test-emails-and-phones) for the full contract.
+
+## Organizations
+
+```sh
+# List
+clerk api /organizations
+clerk api '/organizations?limit=20&query=acme'
+
+# Fetch
+clerk api /organizations/org_abc123
+
+# Create
+clerk api /organizations -d '{"name":"Acme","created_by":"user_abc123"}'
+
+# Update
+clerk api /organizations/org_abc123 -X PATCH -d '{"name":"Acme Inc."}'
+
+# Members
+clerk api /organizations/org_abc123/memberships
+clerk api /organizations/org_abc123/memberships -d '{"user_id":"user_xyz","role":"org:member"}'
+clerk api /organizations/org_abc123/memberships/user_xyz -X PATCH -d '{"role":"org:admin"}'
+clerk api /organizations/org_abc123/memberships/user_xyz -X DELETE --dry-run
+
+# Invitations
+clerk api /organizations/org_abc123/invitations -d '{"email_address":"new@acme.com","role":"org:member"}'
+```
+
+If organization endpoints return `organization_not_enabled_in_instance`, enable the feature first:
+
+```sh
+# Inspect org settings
+clerk api /instance/organization_settings
+
+# Preview, then enable organizations for this instance
+clerk api /instance/organization_settings -X PATCH -d '{"enabled":true}' --dry-run
+clerk api /instance/organization_settings -X PATCH -d '{"enabled":true}' --yes
+```
+
+## Sessions
+
+```sh
+# List active sessions for a user
+clerk api '/sessions?user_id=user_abc123&status=active'
+
+# Revoke a session
+clerk api /sessions/sess_abc123/revoke -X POST
+
+# Create an impersonation / sign-in token (for testing)
+clerk api /sign_in_tokens -d '{"user_id":"user_abc123"}'
+```
+
+## Invitations (top-level, not org-scoped)
+
+```sh
+clerk api /invitations
+clerk api /invitations -d '{"email_address":"new@example.com","redirect_url":"https://example.com/welcome"}'
+clerk api /invitations/inv_abc123/revoke -X POST
+```
+
+## JWT templates
+
+```sh
+clerk api /jwt_templates
+clerk api /jwt_templates/jtmp_abc123
+clerk api /jwt_templates -d '{
+ "name": "supabase",
+ "claims": {"aud": "authenticated", "role": "authenticated"},
+ "lifetime": 60
+}'
+```
+
+## Instance configuration
+
+Prefer the dedicated `config` commands over raw `api` calls - they handle confirmation, dry-run, and formatting.
+
+```sh
+# Pull the current dev config
+clerk config pull
+clerk config pull --output config.dev.json
+
+# Pull production
+clerk config pull --instance prod --output config.prod.json
+
+# Look at the schema to know what's available
+clerk config schema --keys session sign_in social
+
+# PATCH: surgical updates
+clerk config patch --json '{"session":{"lifetime":3600}}' --dry-run
+clerk config patch --json '{"session":{"lifetime":3600}}' --yes
+
+# PUT: replace everything (destructive - always --dry-run first)
+clerk config put --file config.prod.json --dry-run
+clerk config put --file config.prod.json --instance prod --yes
+```
+
+## Environment variables
+
+```sh
+# Pull dev keys into .env.local (auto-detects framework and key names)
+clerk env pull
+
+# Pull production keys
+clerk env pull --instance prod
+
+# Target a specific file
+clerk env pull --file .env
+```
+
+`env pull` merges into the existing file: existing Clerk keys are updated in place; new ones are appended under a `# Clerk` header; everything else is preserved.
+
+## Applications (Platform API)
+
+```sh
+# List your apps
+clerk apps list
+clerk apps list --json
+
+# Fetch one (raw API)
+clerk api /v1/platform/applications/app_abc123 --platform
+```
+
+## Scripting patterns
+
+### Save large responses to a file before reading them
+
+`users list`, `apps list`, `config pull`, and most `clerk api` GETs can return responses ranging from kilobytes to megabytes. Reading the full payload into an LLM-driven session burns context for no benefit. Persist the response, then query just the slice you need:
+
+```sh
+# Persist once, query as many times as you need.
+clerk users list --json --limit 250 > /tmp/users.json
+
+jq '.data | length' /tmp/users.json # count rows on the page
+jq '.hasMore' /tmp/users.json # any more pages?
+jq '.data[0] | keys' /tmp/users.json # learn the shape of one record
+jq '.data[] | {id, email_addresses}' /tmp/users.json # project to relevant fields only
+```
+
+If `jq` is not on `PATH`, fall back to Python or Node, which most environments have:
+
+```sh
+python3 -c 'import json; d=json.load(open("/tmp/users.json")); print(len(d["data"]), d["hasMore"])'
+node -e 'const d=require("/tmp/users.json"); console.log(d.data.length, d.hasMore)'
+```
+
+Only `cat`/`head` the file when you genuinely need the raw structure for one-off debugging.
+
+### Pipe to `jq`
+
+For small responses (or one-shot lookups), inline piping to `jq` is fine:
+
+```sh
+# Get a list of user IDs from the current page (the page envelope is `{ data, hasMore }`)
+clerk users list --json | jq -r '.data[] | .id'
+
+# Count banned users on the current page
+clerk users list --json | jq '[.data[] | select(.banned)] | length'
+
+# Walk every page until hasMore is false. Save each page to its own file so you
+# can inspect them independently without re-fetching.
+offset=0
+while :; do
+ page="/tmp/users-${offset}.json"
+ clerk users list --json --limit 250 --offset "$offset" > "$page"
+ jq -r '.data[] | .id' "$page"
+ [ "$(jq -r '.hasMore' "$page")" = "true" ] || break
+ offset=$((offset + 250))
+done
+```
+
+### Read body from stdin
+
+```sh
+echo '{"first_name":"Bob"}' | clerk api /users/user_abc123 -X PATCH
+jq -n '{email_address:["c@d.co"]}' | clerk api /users
+```
+
+### Loop safely
+
+```sh
+# Always --dry-run first across the whole set. `users list` paginates;
+# bump --limit (max 250) and walk pages with --offset until .hasMore is false.
+for id in $(clerk users list --json --limit 250 | jq -r '.data[] | .id'); do
+ clerk api /users/$id -X PATCH -d '{"public_metadata":{"migrated":true}}' --dry-run
+done
+# Re-run without --dry-run once the previews look right
+```
+
+### Target multiple instances
+
+```sh
+# Copy config from dev to staging for review
+clerk config pull --instance dev --output /tmp/dev-config.json
+clerk config patch --instance ins_staging --file /tmp/dev-config.json --dry-run
+```
+
+## When in doubt
+
+```sh
+clerk api ls # find the right endpoint
+clerk --help # authoritative flag list
+clerk doctor --json # health check
+```
diff --git a/.agents/skills/clerk-custom-ui/SKILL.md b/.agents/skills/clerk-custom-ui/SKILL.md
new file mode 100644
index 0000000..e6e05dc
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/SKILL.md
@@ -0,0 +1,175 @@
+---
+name: clerk-custom-ui
+description: Custom authentication flows and component appearance - hooks (useSignIn,
+ useSignUp), themes, colors, fonts, CSS. Use for custom sign-in/sign-up flows, appearance
+ styling, visual customization, branding.
+allowed-tools: WebFetch
+license: MIT
+metadata:
+ author: clerk
+ version: 2.3.0
+---
+
+# Custom UI
+
+> **Prerequisite**: Ensure `ClerkProvider` wraps your app. See `clerk-setup` skill.
+>
+> **Version**: Check `package.json` for the SDK version — see `clerk` skill for the version table. This determines which custom flow references to use below.
+
+This skill covers two areas:
+1. **Custom authentication flows** — build your own sign-in/sign-up UI with hooks
+2. **Appearance customization** — theme, style, and brand Clerk's pre-built components
+
+## What Do You Need?
+
+| Task | Reference |
+|------|-----------|
+| Custom sign-in (Core 2 / LTS) | core-2/custom-sign-in.md |
+| Custom sign-up (Core 2 / LTS) | core-2/custom-sign-up.md |
+| Custom sign-in (Current SDK v7+) | core-3/custom-sign-in.md |
+| Custom sign-up (Current SDK v7+) | core-3/custom-sign-up.md |
+| Show component pattern (Current SDK) | core-3/show-component.md |
+
+## Custom Flow References
+
+| Task | Core 2 | Current |
+|------|--------|---------|
+| Custom sign-in (useSignIn) | `core-2/custom-sign-in.md` | `core-3/custom-sign-in.md` |
+| Custom sign-up (useSignUp) | `core-2/custom-sign-up.md` | `core-3/custom-sign-up.md` |
+| `` component | *(use ``, ``, ``)* | `core-3/show-component.md` |
+
+---
+
+## Appearance Customization
+
+Appearance customization applies to both Core 2 and the current SDK.
+
+### Component Customization Options
+
+| Task | Documentation |
+|------|---------------|
+| Appearance prop overview | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/overview |
+| Options (structure, logo, buttons) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/layout |
+| Themes (pre-built dark/light) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/themes |
+| Variables (colors, fonts, spacing) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/variables |
+| CAPTCHA configuration | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/captcha |
+| Bring your own CSS | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/bring-your-own-css |
+
+### Appearance Pattern
+
+```typescript
+
+```
+
+> **Core 2 ONLY (skip if current SDK):** The `options` property was named `layout`. Use `layout: { logoImageUrl: '...', socialButtonsVariant: '...' }` instead of `options`.
+
+### variables (colors, typography, borders)
+
+| Property | Description |
+|----------|-------------|
+| `colorPrimary` | Primary color throughout |
+| `colorBackground` | Background color |
+| `borderRadius` | Border radius (default: `0.375rem`) |
+
+**Opacity change:** `colorRing` and `colorModalBackdrop` now render at full opacity. Use explicit `rgba()` values if you need transparency.
+
+> **Core 2 ONLY (skip if current SDK):** `colorRing` and `colorModalBackdrop` rendered at 15% opacity by default.
+
+### options (structure, logo, social buttons)
+
+| Property | Description |
+|----------|-------------|
+| `logoImageUrl` | URL to custom logo |
+| `socialButtonsVariant` | `'blockButton'` \| `'iconButton'` \| `'auto'` |
+| `socialButtonsPlacement` | `'top'` \| `'bottom'` |
+| `showOptionalFields` | Show optional fields (default: `false`) |
+
+> **Core 2 ONLY (skip if current SDK):** This property is called `layout`, not `options`. Also, `showOptionalFields` defaulted to `true`.
+
+### Themes
+
+Install themes from `@clerk/ui`:
+
+```bash
+npm install @clerk/ui
+```
+
+```typescript
+import { dark } from '@clerk/ui/themes'
+
+
+```
+
+> **Core 2 ONLY (skip if current SDK):** Themes are imported from `@clerk/themes` (install `@clerk/themes` instead of `@clerk/ui`). Example: `import { dark } from '@clerk/themes'`.
+
+**Theme stacking** — pass an array where the last theme takes precedence:
+
+```typescript
+import { dark, neobrutalism } from '@clerk/ui/themes'
+
+
+```
+
+**CSS `color-scheme` support** — the default theme respects the CSS `color-scheme` property for automatic light/dark mode toggling.
+
+**Available themes:** `dark`, `neobrutalism`, `shadcn`, `simple`
+
+#### shadcn Theme
+
+> **IMPORTANT:** If the project has `components.json` (shadcn/ui installed), ALWAYS apply the shadcn theme as the FIRST customization step. This ensures Clerk components visually match the existing shadcn design system. Skip this only if the user explicitly requests a different theme.
+
+**If the project uses shadcn/ui** (check for `components.json` in the project root), **always use the shadcn theme**:
+
+```typescript
+import { shadcn } from '@clerk/ui/themes'
+
+
+```
+
+Also import shadcn CSS in your global styles:
+```css
+@import 'tailwindcss';
+@import '@clerk/ui/themes/shadcn.css';
+```
+
+> **Core 2 ONLY (skip if current SDK):** Import from `@clerk/themes` and `@clerk/themes/shadcn.css`:
+> ```typescript
+> import { shadcn } from '@clerk/themes'
+> ```
+> ```css
+> @import '@clerk/themes/shadcn.css';
+> ```
+
+## Workflow
+
+1. Identify customization needs (custom flow or appearance)
+2. For custom flows: check SDK version → read appropriate `core-2/` or `core-3/` reference
+3. For appearance: WebFetch the appropriate documentation from table above
+4. Apply appearance prop to your Clerk components or build custom flow with hooks
+
+## Common Pitfalls
+
+| Issue | Solution |
+|-------|----------|
+| Colors not applying | Use `colorPrimary` not `primaryColor` |
+| Logo not showing | Put `logoImageUrl` inside `options: {}` (or `layout: {}` in Core 2) |
+| Social buttons wrong | Add `socialButtonsVariant: 'iconButton'` in `options` (or `layout` in Core 2) |
+| Styling not working | Use appearance prop, not direct CSS (unless with bring-your-own-css) |
+| Hook returns different shape | Check SDK version — Core 2 and current have completely different `useSignIn`/`useSignUp` APIs |
+
+## See Also
+
+- `clerk-setup` - Initial Clerk install
+- `clerk-nextjs-patterns` - Next.js patterns
+- `clerk-orgs` - B2B organizations
diff --git a/.agents/skills/clerk-custom-ui/core-2/custom-sign-in.md b/.agents/skills/clerk-custom-ui/core-2/custom-sign-in.md
new file mode 100644
index 0000000..28759de
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/core-2/custom-sign-in.md
@@ -0,0 +1,224 @@
+# Custom Sign-In Flow (Core 2)
+
+> This document covers the **older SDK** (`@clerk/nextjs` v5–v6, `@clerk/clerk-react` v5–v6, `@clerk/clerk-expo` v1–v2). For the current SDK, see `core-3/custom-sign-in.md`.
+
+Build a custom sign-in experience using the `useSignIn()` hook.
+
+## Hook API
+
+```typescript
+import { useSignIn } from '@clerk/nextjs' // or @clerk/clerk-react, @clerk/clerk-expo
+
+const { signIn, isLoaded, setActive } = useSignIn()
+```
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `signIn` | `SignIn` | Sign-in object with methods |
+| `isLoaded` | `boolean` | Whether the hook has loaded |
+| `setActive` | `(params) => Promise` | Sets the active session |
+
+## Sign-In Flow
+
+### 1. Create Sign-In
+
+```typescript
+const result = await signIn.create({
+ identifier: 'user@example.com',
+ password: 'securePassword123',
+})
+```
+
+### 2. First Factor Verification
+
+If additional verification is needed (email code, phone code):
+
+```typescript
+// Prepare first factor
+await signIn.prepareFirstFactor({
+ strategy: 'email_code', // or 'phone_code'
+})
+
+// Attempt first factor
+const result = await signIn.attemptFirstFactor({
+ strategy: 'email_code',
+ code: '123456',
+})
+```
+
+### 3. Second Factor (MFA)
+
+If the sign-in requires MFA:
+
+```typescript
+// Prepare second factor
+await signIn.prepareSecondFactor({
+ strategy: 'email_code', // or 'phone_code'
+})
+
+// Attempt second factor
+const result = await signIn.attemptSecondFactor({
+ strategy: 'totp', // or 'email_code', 'phone_code', 'backup_code'
+ code: '123456',
+})
+```
+
+### 4. Finalize
+
+Set the active session after successful authentication:
+
+```typescript
+await setActive({ session: signIn.createdSessionId })
+```
+
+### Password Reset
+
+```typescript
+// 1. Start reset flow
+await signIn.create({ strategy: 'reset_password_email_code', identifier: 'user@example.com' })
+
+// or prepare after initial create:
+await signIn.prepareFirstFactor({ strategy: 'reset_password_email_code' })
+
+// 2. Verify reset code
+await signIn.attemptFirstFactor({ strategy: 'reset_password_email_code', code: '123456' })
+
+// 3. Set new password
+await signIn.resetPassword({ password: 'newSecurePassword123' })
+```
+
+### SSO (OAuth)
+
+```typescript
+await signIn.authenticateWithRedirect({
+ strategy: 'oauth_google', // or 'oauth_github', etc.
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+})
+```
+
+## Error Handling
+
+Use try/catch with `isClerkAPIResponseError()`:
+
+```typescript
+import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
+
+try {
+ await signIn.create({ identifier, password })
+} catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ err.errors.forEach((e) => {
+ console.log(e.code) // e.g. 'form_identifier_not_found'
+ console.log(e.message) // Human-readable message
+ console.log(e.longMessage) // Detailed message
+ })
+ }
+}
+```
+
+## Complete Example: Email/Password with MFA
+
+```tsx
+'use client'
+import { useState } from 'react'
+import { useSignIn } from '@clerk/nextjs'
+import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
+import { useRouter } from 'next/navigation'
+
+export default function SignInPage() {
+ const { signIn, isLoaded, setActive } = useSignIn()
+ const router = useRouter()
+
+ const [identifier, setIdentifier] = useState('')
+ const [password, setPassword] = useState('')
+ const [mfaCode, setMfaCode] = useState('')
+ const [step, setStep] = useState<'credentials' | 'mfa'>('credentials')
+ const [error, setError] = useState('')
+
+ if (!isLoaded) return Loading...
+
+ async function handleSignIn(e: React.FormEvent) {
+ e.preventDefault()
+ setError('')
+
+ try {
+ const result = await signIn.create({ identifier, password })
+
+ if (result.status === 'needs_second_factor') {
+ setStep('mfa')
+ return
+ }
+
+ if (result.status === 'complete') {
+ await setActive({ session: result.createdSessionId })
+ router.push('/')
+ }
+ } catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ setError(err.errors[0]?.message || 'Sign in failed')
+ }
+ }
+ }
+
+ async function handleMFA(e: React.FormEvent) {
+ e.preventDefault()
+ setError('')
+
+ try {
+ const result = await signIn.attemptSecondFactor({
+ strategy: 'totp',
+ code: mfaCode,
+ })
+
+ if (result.status === 'complete') {
+ await setActive({ session: result.createdSessionId })
+ router.push('/')
+ }
+ } catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ setError(err.errors[0]?.message || 'Verification failed')
+ }
+ }
+ }
+
+ if (step === 'mfa') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+```
+
+## Docs
+
+- [Custom sign-in flow](https://clerk.com/docs/custom-flows/overview)
+- [useSignIn() reference](https://clerk.com/docs/references/react/use-sign-in)
diff --git a/.agents/skills/clerk-custom-ui/core-2/custom-sign-up.md b/.agents/skills/clerk-custom-ui/core-2/custom-sign-up.md
new file mode 100644
index 0000000..574dbe7
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/core-2/custom-sign-up.md
@@ -0,0 +1,190 @@
+# Custom Sign-Up Flow (Core 2)
+
+> This document covers the **older SDK** (`@clerk/nextjs` v5–v6, `@clerk/clerk-react` v5–v6, `@clerk/clerk-expo` v1–v2). For the current SDK, see `core-3/custom-sign-up.md`.
+
+Build a custom sign-up experience using the `useSignUp()` hook.
+
+## Hook API
+
+```typescript
+import { useSignUp } from '@clerk/nextjs' // or @clerk/clerk-react, @clerk/clerk-expo
+
+const { signUp, isLoaded, setActive } = useSignUp()
+```
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `signUp` | `SignUp` | Sign-up object with methods |
+| `isLoaded` | `boolean` | Whether the hook has loaded |
+| `setActive` | `(params) => Promise` | Sets the active session |
+
+## Sign-Up Flow
+
+### 1. Create Sign-Up
+
+```typescript
+const result = await signUp.create({
+ emailAddress: 'user@example.com',
+ password: 'securePassword123',
+ firstName: 'Jane', // optional
+ lastName: 'Doe', // optional
+})
+```
+
+### 2. Prepare Verification
+
+Send a verification code to the user's email or phone:
+
+```typescript
+await signUp.prepareVerification({
+ strategy: 'email_code', // or 'phone_code', 'email_link'
+})
+```
+
+### 3. Attempt Verification
+
+Verify the code the user received:
+
+```typescript
+const result = await signUp.attemptVerification({
+ strategy: 'email_code',
+ code: '123456',
+})
+```
+
+### 4. Finalize
+
+Set the active session after successful sign-up:
+
+```typescript
+await setActive({ session: signUp.createdSessionId })
+```
+
+### SSO (OAuth)
+
+```typescript
+await signUp.authenticateWithRedirect({
+ strategy: 'oauth_google',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+})
+```
+
+## Error Handling
+
+Use try/catch with `isClerkAPIResponseError()`:
+
+```typescript
+import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
+
+try {
+ await signUp.create({ emailAddress, password })
+} catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ err.errors.forEach((e) => {
+ console.log(e.code) // e.g. 'form_password_pwned'
+ console.log(e.message) // Human-readable message
+ console.log(e.longMessage) // Detailed message
+ })
+ }
+}
+```
+
+## Complete Example: Email/Password with Email Verification
+
+```tsx
+'use client'
+import { useState } from 'react'
+import { useSignUp } from '@clerk/nextjs'
+import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
+import { useRouter } from 'next/navigation'
+
+export default function SignUpPage() {
+ const { signUp, isLoaded, setActive } = useSignUp()
+ const router = useRouter()
+
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [code, setCode] = useState('')
+ const [step, setStep] = useState<'register' | 'verify'>('register')
+ const [error, setError] = useState('')
+
+ if (!isLoaded) return Loading...
+
+ async function handleRegister(e: React.FormEvent) {
+ e.preventDefault()
+ setError('')
+
+ try {
+ await signUp.create({ emailAddress: email, password })
+ await signUp.prepareVerification({ strategy: 'email_code' })
+ setStep('verify')
+ } catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ setError(err.errors[0]?.message || 'Sign up failed')
+ }
+ }
+ }
+
+ async function handleVerify(e: React.FormEvent) {
+ e.preventDefault()
+ setError('')
+
+ try {
+ const result = await signUp.attemptVerification({
+ strategy: 'email_code',
+ code,
+ })
+
+ if (result.status === 'complete') {
+ await setActive({ session: result.createdSessionId })
+ router.push('/')
+ }
+ } catch (err) {
+ if (isClerkAPIResponseError(err)) {
+ setError(err.errors[0]?.message || 'Verification failed')
+ }
+ }
+ }
+
+ if (step === 'verify') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+```
+
+## Docs
+
+- [Custom sign-up flow](https://clerk.com/docs/custom-flows/overview)
+- [useSignUp() reference](https://clerk.com/docs/references/react/use-sign-up)
diff --git a/.agents/skills/clerk-custom-ui/core-3/custom-sign-in.md b/.agents/skills/clerk-custom-ui/core-3/custom-sign-in.md
new file mode 100644
index 0000000..50f13c2
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/core-3/custom-sign-in.md
@@ -0,0 +1,314 @@
+# Custom Sign-In Flow
+
+Build a custom sign-in experience using the `useSignIn()` hook.
+
+## Hook API
+
+```typescript
+import { useSignIn } from '@clerk/nextjs' // or @clerk/react, @clerk/expo
+
+const { signIn, errors, fetchStatus } = useSignIn()
+```
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `signIn` | `SignInFuture` | Sign-in object with namespaced methods |
+| `errors` | `Errors` | Structured error object |
+| `fetchStatus` | `'idle' \| 'fetching'` | Network request status |
+
+## Sign-In Methods
+
+### Password
+
+```typescript
+const { error } = await signIn.password({
+ identifier: 'user@example.com',
+ password: 'securePassword123',
+})
+```
+
+### SSO (OAuth / Enterprise)
+
+```typescript
+const { error } = await signIn.sso({
+ strategy: 'oauth_google', // or 'oauth_github', 'enterprise_sso', etc.
+ redirectUrl: '/dashboard', // where to go after SSO completes
+ redirectCallbackUrl: '/sso-callback', // intermediate callback route
+})
+```
+
+### Passkey
+
+```typescript
+const { error } = await signIn.passkey({ flow: 'discoverable' })
+```
+
+### Web3
+
+```typescript
+const { error } = await signIn.web3({ strategy: 'web3_solana_signature' })
+// or
+const { error } = await signIn.web3({ strategy: 'web3_base_signature' })
+```
+
+### Ticket (Invitation link)
+
+```typescript
+const { error } = await signIn.ticket({ ticket: 'ticket_abc123' })
+```
+
+### Email Code
+
+```typescript
+// Send code (emailAddress is optional if a signIn already exists from a prior method call)
+const { error } = await signIn.emailCode.sendCode({ emailAddress: 'user@example.com' })
+
+// Verify code
+const { error } = await signIn.emailCode.verifyCode({ code: '123456' })
+```
+
+### Phone Code
+
+```typescript
+// Send code (phoneNumber is optional if a signIn already exists from a prior method call)
+const { error } = await signIn.phoneCode.sendCode({ phoneNumber: '+12015551234' })
+
+// Verify code
+const { error } = await signIn.phoneCode.verifyCode({ code: '123456' })
+```
+
+## MFA (Second Factor)
+
+A second factor is required when `signIn.status` is one of:
+- `'needs_second_factor'` — user has MFA enabled (TOTP, backup codes, etc.)
+- `'needs_client_trust'` — new device sign-in without MFA; requires email or phone code verification
+
+```typescript
+// TOTP (Authenticator app)
+const { error } = await signIn.mfa.verifyTOTP({ code: '123456' })
+
+// Backup code
+const { error } = await signIn.mfa.verifyBackupCode({ code: 'backup-code-here' })
+
+// Email code
+const { error: sendErr } = await signIn.mfa.sendEmailCode()
+const { error: verifyErr } = await signIn.mfa.verifyEmailCode({ code: '123456' })
+
+// Phone code
+const { error: sendErr } = await signIn.mfa.sendPhoneCode()
+const { error: verifyErr } = await signIn.mfa.verifyPhoneCode({ code: '123456' })
+```
+
+## Password Reset
+
+```typescript
+// 1. Send reset code
+const { error } = await signIn.resetPasswordEmailCode.sendCode()
+
+// 2. Verify the code
+const { error } = await signIn.resetPasswordEmailCode.verifyCode({ code: '123456' })
+
+// 3. Submit new password
+const { error } = await signIn.resetPasswordEmailCode.submitPassword({
+ password: 'newSecurePassword123',
+})
+```
+
+## Client Trust
+
+When a user signs in with a valid password from a new device without MFA enabled, the sign-in status becomes `needs_client_trust`. This requires an additional verification step:
+
+```typescript
+if (signIn.status === 'needs_client_trust') {
+ // Check supportedSecondFactors for available methods (email_code or phone_code)
+ const factors = signIn.supportedSecondFactors
+ // Use the appropriate mfa method to verify
+}
+```
+
+## Finalizing Sign-In
+
+After successful authentication, call `finalize()` to activate the session:
+
+```typescript
+await signIn.finalize({
+ navigate: async ({ session, decorateUrl }) => {
+ const destination = session.currentTask
+ ? `/sign-in/tasks/${session.currentTask.key}`
+ : '/'
+ const url = decorateUrl(destination)
+ // decorateUrl may return an absolute URL for Safari ITP
+ if (url.startsWith('http')) {
+ window.location.href = url
+ } else {
+ router.push(url)
+ }
+ },
+})
+```
+
+- `decorateUrl(path)` — decorates the URL with session info (required to support Safari's Intelligent Tracking Prevention). May return an absolute URL.
+- `session.currentTask` — check for pending session tasks before redirecting
+
+### Reset State
+
+Clear local sign-in state and start over:
+
+```typescript
+signIn.reset()
+```
+
+## Error Handling
+
+All methods return `Promise<{ error: ClerkError | null }>`. Errors are also available reactively on the hook:
+
+```typescript
+const { signIn, errors } = useSignIn()
+
+// Field-level errors
+errors?.fields?.identifier // { code, message, longMessage? }
+errors?.fields?.password // { code, message, longMessage? }
+errors?.fields?.code // { code, message, longMessage? }
+
+// Global errors (not tied to a field)
+errors?.global // ClerkGlobalHookError[] | null
+
+// Raw error array
+errors?.raw // ClerkError[] | null
+```
+
+## Complete Example: Email/Password with MFA
+
+From [the docs](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication). Supports SMS verification codes, authenticator app (TOTP), and backup codes.
+
+```tsx
+'use client'
+
+import { useSignIn } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function Page() {
+ const { signIn, errors, fetchStatus } = useSignIn()
+ const router = useRouter()
+
+ const handleSubmit = async (formData: FormData) => {
+ const emailAddress = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ await signIn.password({
+ emailAddress,
+ password,
+ })
+
+ // If you're using the authenticator app strategy, remove this check.
+ if (signIn.status === 'needs_second_factor') {
+ await signIn.mfa.sendPhoneCode()
+ }
+
+ if (signIn.status === 'complete') {
+ await signIn.finalize({
+ navigate: ({ session, decorateUrl }) => {
+ if (session?.currentTask) {
+ // Handle pending session tasks
+ // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
+ console.log(session?.currentTask)
+ return
+ }
+
+ const url = decorateUrl('/')
+ if (url.startsWith('http')) {
+ window.location.href = url
+ } else {
+ router.push(url)
+ }
+ },
+ })
+ }
+ }
+
+ const handleMFAVerification = async (formData: FormData) => {
+ const code = formData.get('code') as string
+ const useBackupCode = formData.get('useBackupCode') === 'on'
+
+ if (useBackupCode) {
+ await signIn.mfa.verifyBackupCode({ code })
+ } else {
+ await signIn.mfa.verifyPhoneCode({ code })
+ // If you're using the authenticator app strategy, use the following method instead:
+ // await signIn.mfa.verifyTOTP({ code })
+ }
+
+ if (signIn.status === 'complete') {
+ await signIn.finalize({
+ navigate: ({ session, decorateUrl }) => {
+ if (session?.currentTask) {
+ // Handle pending session tasks
+ // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
+ console.log(session?.currentTask)
+ return
+ }
+
+ const url = decorateUrl('/')
+ if (url.startsWith('http')) {
+ window.location.href = url
+ } else {
+ router.push(url)
+ }
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'needs_second_factor') {
+ return (
+
+
Verify your account
+
+
+ )
+ }
+
+ return (
+ <>
+ Sign in
+
+ {errors && {JSON.stringify(errors, null, 2)}
}
+ >
+ )
+}
+```
+
+## Docs
+
+- [Custom sign-in flow](https://clerk.com/docs/custom-flows/overview)
+- [MFA custom flow](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication)
+- [useSignIn() reference](https://clerk.com/docs/references/react/use-sign-in)
diff --git a/.agents/skills/clerk-custom-ui/core-3/custom-sign-up.md b/.agents/skills/clerk-custom-ui/core-3/custom-sign-up.md
new file mode 100644
index 0000000..ac243ba
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/core-3/custom-sign-up.md
@@ -0,0 +1,259 @@
+# Custom Sign-Up Flow
+
+Build a custom sign-up experience using the `useSignUp()` hook.
+
+## Hook API
+
+```typescript
+import { useSignUp } from '@clerk/nextjs' // or @clerk/react, @clerk/expo
+
+const { signUp, errors, fetchStatus } = useSignUp()
+```
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `signUp` | `SignUpFuture` | Sign-up object with namespaced methods |
+| `errors` | `Errors` | Structured error object |
+| `fetchStatus` | `'idle' \| 'fetching'` | Network request status |
+
+## Sign-Up Methods
+
+### Password (Email/Password)
+
+```typescript
+const { error } = await signUp.password({
+ emailAddress: 'user@example.com',
+ password: 'securePassword123',
+ firstName: 'Jane', // optional
+ lastName: 'Doe', // optional
+})
+```
+
+### SSO (OAuth)
+
+```typescript
+const { error } = await signUp.sso({
+ strategy: 'oauth_google', // or 'oauth_github', etc.
+ redirectUrl: '/dashboard', // where to go after SSO completes
+ redirectCallbackUrl: '/sso-callback', // intermediate callback route
+})
+```
+
+### Web3
+
+```typescript
+const { error } = await signUp.web3({ strategy: 'web3_solana_signature' })
+```
+
+### Update (add fields to existing sign-up)
+
+Use `update()` to add optional fields (name, metadata, legal acceptance, locale) to an existing sign-up before finalization.
+
+```typescript
+const { error } = await signUp.update({
+ firstName: 'Jane',
+ lastName: 'Doe',
+ unsafeMetadata: { referralSource: 'twitter' },
+ legalAccepted: true,
+})
+```
+
+## Email / Phone Verification
+
+After creating a sign-up, verify the user's email or phone:
+
+### Email Code
+
+```typescript
+// Send verification code
+const { error } = await signUp.verifications.sendEmailCode()
+
+// Verify the code
+const { error } = await signUp.verifications.verifyEmailCode({ code: '123456' })
+```
+
+### Phone Code
+
+```typescript
+// Send verification code
+const { error } = await signUp.verifications.sendPhoneCode()
+
+// Verify the code
+const { error } = await signUp.verifications.verifyPhoneCode({ code: '123456' })
+```
+
+### Email Link
+
+```typescript
+// verificationUrl: where the user lands after clicking the email link (relative or absolute)
+const { error } = await signUp.verifications.sendEmailLink({ verificationUrl: '/verify' })
+// User clicks the link in their email to verify
+```
+
+## Finalizing Sign-Up
+
+After successful sign-up and verification, call `finalize()` to activate the session:
+
+```typescript
+await signUp.finalize({
+ navigate: async ({ session, decorateUrl }) => {
+ const destination = session.currentTask
+ ? `/sign-up/tasks/${session.currentTask.key}`
+ : '/'
+ const url = decorateUrl(destination)
+ // decorateUrl may return an absolute URL for Safari ITP
+ if (url.startsWith('http')) {
+ window.location.href = url
+ } else {
+ router.push(url)
+ }
+ },
+})
+```
+
+### Transferable Sign-Ups
+
+If `signUp.isTransferable` is `true`, the identifier matches an existing user and the sign-up should be transferred to a sign-in flow. This involves coordinating between sign-up and sign-in resources. See the [transferable sign-up docs](https://clerk.com/docs/custom-flows/overview) for the full implementation.
+
+### Reset State
+
+Clear local sign-up state and start over:
+
+```typescript
+signUp.reset()
+```
+
+## Error Handling
+
+All methods return `Promise<{ error: ClerkError | null }>`. Errors are also available reactively on the hook:
+
+```typescript
+const { signUp, errors } = useSignUp()
+
+// Field-level errors
+errors?.fields?.emailAddress // { code, message, longMessage? }
+errors?.fields?.password // { code, message, longMessage? }
+errors?.fields?.firstName // { code, message, longMessage? }
+errors?.fields?.lastName // { code, message, longMessage? }
+errors?.fields?.phoneNumber // { code, message, longMessage? }
+errors?.fields?.username // { code, message, longMessage? }
+errors?.fields?.code // { code, message, longMessage? }
+
+// Global errors
+errors?.global // ClerkGlobalHookError[] | null
+
+// Raw error array
+errors?.raw // ClerkError[] | null
+```
+
+## Complete Example: Phone OTP Sign-Up
+
+From [the docs](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp). Uses phone OTP with inline comments for adapting to email OTP.
+
+```tsx
+'use client'
+
+import * as React from 'react'
+import { useAuth, useSignUp } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function SignUpPage() {
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const { isSignedIn } = useAuth()
+ const router = useRouter()
+
+ const handleSubmit = async (formData: FormData) => {
+ // For email OTP: collect the email address instead of the phone number
+ const phoneNumber = formData.get('phoneNumber') as string
+
+ // For email OTP: change create({ phoneNumber }) to create({ emailAddress })
+ const error = await signUp.create({ phoneNumber })
+
+ // For email OTP: change sendPhoneCode() to sendEmailCode()
+ if (!error) await signUp.verifications.sendPhoneCode()
+ }
+
+ const handleVerify = async (formData: FormData) => {
+ const code = formData.get('code') as string
+
+ // For email OTP: change verifyPhoneCode() to verifyEmailCode()
+ await signUp.verifications.verifyPhoneCode({ code })
+
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: ({ session, decorateUrl }) => {
+ if (session?.currentTask) {
+ // Handle pending session tasks
+ // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
+ console.log(session?.currentTask)
+ return
+ }
+
+ const url = decorateUrl('/')
+ if (url.startsWith('http')) {
+ window.location.href = url
+ } else {
+ router.push(url)
+ }
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ if (
+ signUp.status === 'missing_requirements' &&
+ // For email OTP: check for phone_number instead of email_address
+ signUp.unverifiedFields.includes('phone_number') &&
+ signUp.missingFields.length === 0
+ ) {
+ return (
+ <>
+ Verify your account
+
+ {/* For email OTP: change sendPhoneCode() to sendEmailCode() */}
+ signUp.verifications.sendPhoneCode()}>I need a new code
+ >
+ )
+ }
+
+ return (
+ <>
+ Sign up
+
+ {errors && {JSON.stringify(errors, null, 2)}
}
+
+ {/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
+
+ >
+ )
+}
+```
+
+## Docs
+
+- [Custom sign-up flow](https://clerk.com/docs/custom-flows/overview)
+- [Email/phone OTP custom flow](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp)
+- [useSignUp() reference](https://clerk.com/docs/references/react/use-sign-up)
diff --git a/.agents/skills/clerk-custom-ui/core-3/show-component.md b/.agents/skills/clerk-custom-ui/core-3/show-component.md
new file mode 100644
index 0000000..f01a8b4
--- /dev/null
+++ b/.agents/skills/clerk-custom-ui/core-3/show-component.md
@@ -0,0 +1,125 @@
+# `` Component
+
+The `` component conditionally renders content based on authentication state, roles, permissions, billing plans, and features.
+
+> **Core 2 ONLY (skip if current SDK):** The `` component does not exist in Core 2. Use ``, ``, and `` instead. See migration table below.
+
+## Import
+
+```typescript
+import { Show } from '@clerk/nextjs' // Next.js
+import { Show } from '@clerk/react' // React
+import { Show } from '@clerk/react-router' // React Router
+import { Show } from '@clerk/expo' // Expo
+```
+
+## Props
+
+| Prop | Type | Description |
+|------|------|-------------|
+| `when` | `string \| object \| function` | Condition for rendering children |
+| `fallback?` | `ReactNode` | Content shown when condition fails |
+| `treatPendingAsSignedOut?` | `boolean` | Treat pending sessions as signed-out (default: `true`) |
+
+## `when` Prop Variants
+
+### Authentication State
+
+```tsx
+// Show content only when signed in
+
+ Welcome back!
+
+
+// Show content only when signed out
+
+ Please sign in.
+
+```
+
+### Role Check
+
+```tsx
+
+
+
+```
+
+### Permission Check
+
+```tsx
+
+
+
+```
+
+### Billing Feature Check
+
+```tsx
+
+
+
+```
+
+### Billing Plan Check
+
+```tsx
+
+
+
+```
+
+### Custom Condition (Function)
+
+```tsx
+ has({ role: 'org:admin' }) || has({ permission: 'org:billing:manage' })}>
+
+
+```
+
+## Fallback Content
+
+Show alternative content when the condition fails:
+
+```tsx
+Please sign in to continue.
}>
+
+
+```
+
+## Session Tasks and Pending State
+
+The `treatPendingAsSignedOut` prop controls how pending sessions (sessions with incomplete tasks) are handled:
+
+```tsx
+// Default: pending sessions are treated as signed-out
+
+
+
+
+// Treat pending sessions as signed-in (e.g., to show task completion UI)
+
+
+
+```
+
+## Security Caveat
+
+**`` only visually hides content** — it remains in browser source. It is not a security boundary. For protecting sensitive data, always verify authentication server-side with `auth()` or use `auth.protect()` in middleware.
+
+## Migration from Core 2
+
+| Core 2 | Current |
+|--------|---------|
+| `` | `` |
+| `` | `` |
+| `` | `` |
+| `` | `` |
+| ` expr}>` | ` expr}>` |
+| `` | `` |
+| *(no equivalent)* | `` |
+| *(no equivalent)* | `` |
+
+## Docs
+
+- [Show component reference](https://clerk.com/docs/components/control/show)
diff --git a/.agents/skills/clerk-expo-patterns/SKILL.md b/.agents/skills/clerk-expo-patterns/SKILL.md
new file mode 100644
index 0000000..8dab9e0
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/SKILL.md
@@ -0,0 +1,169 @@
+---
+name: clerk-expo-patterns
+description: 'Expo / React Native patterns with Clerk — SecureStore token cache, OAuth
+ deep linking, useAuth in native, Expo Router protected routes, push notifications
+ with user context. Triggers on: expo clerk, clerk react native, SecureStore token
+ cache, expo router auth, OAuth deep link clerk, mobile auth clerk.'
+license: MIT
+allowed-tools: WebFetch
+metadata:
+ author: clerk
+ version: 1.0.0
+compatibility: Requires Expo CLI and expo-secure-store package
+---
+
+# Expo Patterns
+
+SDK: `@clerk/expo` v3+. Requires Expo 53+, React Native 0.73+.
+
+## What Do You Need?
+
+| Task | Reference |
+|------|-----------|
+| Persist tokens with SecureStore | references/token-storage.md |
+| OAuth (Google, Apple, GitHub) | references/oauth-deep-linking.md |
+| Protected screens with Expo Router | references/protected-routes.md |
+| Push notifications with user data | references/push-notifications.md |
+
+## Mental Model
+
+Clerk stores the session token in memory by default. In native apps:
+
+- **SecureStore** — encrypt token in device keychain (recommended for production)
+- **`tokenCache`** — prop on `` that provides custom storage
+- **`useAuth`** — same API as web, works in any component
+- **OAuth** — requires `useSSO` + deep link scheme configured in `app.json`
+
+## Minimal Setup
+
+### app/_layout.tsx
+
+```tsx
+import { ClerkProvider } from '@clerk/expo'
+import { tokenCache } from '@clerk/expo/token-cache'
+import { Stack } from 'expo-router'
+
+const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
+
+export default function RootLayout() {
+ return (
+
+
+
+ )
+}
+```
+
+> **CRITICAL**: Use `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` — not `NEXT_PUBLIC_`. Env vars inside `node_modules` are not inlined in production builds. Always pass `publishableKey` explicitly.
+
+## Built-in Token Cache
+
+```tsx
+import { tokenCache } from '@clerk/expo/token-cache'
+```
+
+This uses `expo-secure-store` with `keychainAccessible: AFTER_FIRST_UNLOCK`. Install the peer dep:
+
+```bash
+npx expo install expo-secure-store
+```
+
+## Auth Hooks
+
+```tsx
+import { useAuth, useUser, useSignIn, useSignUp, useClerk } from '@clerk/expo'
+
+export function ProfileScreen() {
+ const { isSignedIn, userId, signOut } = useAuth()
+ const { user } = useUser()
+
+ if (!isSignedIn) return
+ return (
+
+ {user?.fullName}
+ signOut()} />
+
+ )
+}
+```
+
+## OAuth Flow (Google)
+
+```tsx
+import { useSSO } from '@clerk/expo'
+import * as WebBrowser from 'expo-web-browser'
+
+WebBrowser.maybeCompleteAuthSession()
+
+export function GoogleSignIn() {
+ const { startSSOFlow } = useSSO()
+
+ const handlePress = async () => {
+ try {
+ const { createdSessionId, setActive } = await startSSOFlow({
+ strategy: 'oauth_google',
+ redirectUrl: 'myapp://oauth-callback',
+ })
+ if (createdSessionId) await setActive!({ session: createdSessionId })
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ return
+}
+```
+
+## Org Switching
+
+```tsx
+import { useOrganization, useOrganizationList } from '@clerk/expo'
+
+export function OrgSwitcher() {
+ const { organization } = useOrganization()
+ const { setActive, userMemberships } = useOrganizationList()
+
+ return (
+
+ Current: {organization?.name ?? 'Personal'}
+ {userMemberships.data?.map(mem => (
+ setActive({ organization: mem.organization.id })}
+ />
+ ))}
+
+ )
+}
+```
+
+## Common Pitfalls
+
+| Symptom | Cause | Fix |
+|---------|-------|-----|
+| `publishableKey` undefined in prod | Using env var without `EXPO_PUBLIC_` | Rename to `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` |
+| Token lost on app restart | No `tokenCache` | Pass `tokenCache` from `@clerk/expo/token-cache` |
+| OAuth redirect not working | Missing scheme in `app.json` | Add `"scheme": "myapp"` to `app.json` |
+| `WebBrowser.maybeCompleteAuthSession` | Not called | Call it at the top level of the OAuth callback screen |
+| `useSSO` not found | Old `@clerk/expo` version | `useSSO` replaced `useOAuth` in v3+ |
+
+## Import Map
+
+| What | Import From |
+|------|-------------|
+| `ClerkProvider` | `@clerk/expo` |
+| `tokenCache` | `@clerk/expo/token-cache` |
+| `useAuth`, `useUser`, `useSignIn` | `@clerk/expo` |
+| `useSSO` | `@clerk/expo` |
+| `useOrganization`, `useOrganizationList` | `@clerk/expo` |
+
+## See Also
+
+- `clerk-setup` - Initial Clerk install
+- `clerk-custom-ui` - Custom flows & appearance
+- `clerk-orgs` - B2B organizations
+
+## Docs
+
+[Expo SDK](https://clerk.com/docs/expo/getting-started/quickstart)
diff --git a/.agents/skills/clerk-expo-patterns/evals/evals.json b/.agents/skills/clerk-expo-patterns/evals/evals.json
new file mode 100644
index 0000000..dc416b6
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/evals/evals.json
@@ -0,0 +1,83 @@
+{
+ "skill_name": "clerk-expo-patterns",
+ "evals": [
+ {
+ "id": 1,
+ "prompt": "i'm building an expo app and want to add clerk. show me how to wrap the app with ClerkProvider and configure the SecureStore token cache so the session persists after app restarts.",
+ "expected_output": "Wraps app in ClerkProvider with publishableKey from EXPO_PUBLIC_ env, passes tokenCache from @clerk/expo/token-cache",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Imports ClerkProvider from @clerk/expo",
+ "Imports tokenCache from @clerk/expo/token-cache",
+ "Reads publishableKey from process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
+ "Passes tokenCache prop to ClerkProvider",
+ "Does NOT use NEXT_PUBLIC_ prefix for the env variable"
+ ]
+ },
+ {
+ "id": 2,
+ "prompt": "i want users to sign in with Google in my expo app. implement the OAuth flow using useSSO and handle the deep link redirect.",
+ "expected_output": "Uses useSSO hook with startSSOFlow, passes redirectUrl with app scheme, calls WebBrowser.maybeCompleteAuthSession",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Imports useSSO from @clerk/expo",
+ "Calls startSSOFlow with strategy: 'oauth_google'",
+ "Passes redirectUrl with the app's custom scheme (e.g. myapp://oauth-callback)",
+ "Calls WebBrowser.maybeCompleteAuthSession() at the top level of the callback screen",
+ "Calls setActive with the createdSessionId after successful OAuth"
+ ]
+ },
+ {
+ "id": 3,
+ "prompt": "my expo router app has a (tabs) group that should only be accessible to signed-in users. redirect unauthenticated users to the sign-in screen.",
+ "expected_output": "Uses useAuth in the tabs layout, redirects to /sign-in with Redirect component when not signed in",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Imports useAuth from @clerk/expo",
+ "Checks isSignedIn and isLoaded from useAuth",
+ "Returns null or loading indicator when isLoaded is false",
+ "Redirects to the sign-in screen when isSignedIn is false",
+ "Uses Expo Router's Redirect component or router.replace for the redirect"
+ ]
+ },
+ {
+ "id": 4,
+ "prompt": "i need the clerk session token to survive app restarts. show me how to implement a custom token cache using expo-secure-store directly instead of the built-in one.",
+ "expected_output": "Creates a TokenCache object with getToken and saveToken using SecureStore.getItemAsync and setItemAsync",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Creates an object implementing TokenCache interface with getToken and saveToken methods",
+ "Uses SecureStore.getItemAsync in getToken",
+ "Uses SecureStore.setItemAsync in saveToken",
+ "Handles errors in getToken by deleting the corrupted item and returning null",
+ "Passes the custom cache as tokenCache prop to ClerkProvider"
+ ]
+ },
+ {
+ "id": 5,
+ "prompt": "i want to show the user's profile information (name, avatar, email) in a profile screen in my expo app.",
+ "expected_output": "Uses useUser hook to access user data, renders user.fullName, user.imageUrl, and primary email",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Imports useUser from @clerk/expo",
+ "Destructures user from useUser()",
+ "Shows user.fullName or user.firstName",
+ "Handles the case where user is null (not yet loaded)",
+ "Accesses email via user.primaryEmailAddress?.emailAddress"
+ ]
+ },
+ {
+ "id": 6,
+ "prompt": "my expo app supports organizations. i need a screen where users can see their organizations and switch the active one.",
+ "expected_output": "Uses useOrganizationList to list memberships and setActive, uses useOrganization to show current org",
+ "scaffold": "expo-basic-auth",
+ "expectations": [
+ "Imports useOrganization and useOrganizationList from @clerk/expo",
+ "Calls setActive from useOrganizationList to switch organizations",
+ "Lists available orgs from userMemberships.data",
+ "Shows the currently active organization name",
+ "Handles the case where userMemberships.data is undefined or loading"
+ ]
+ }
+ ]
+}
diff --git a/.agents/skills/clerk-expo-patterns/references/oauth-deep-linking.md b/.agents/skills/clerk-expo-patterns/references/oauth-deep-linking.md
new file mode 100644
index 0000000..367f2b0
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/references/oauth-deep-linking.md
@@ -0,0 +1,79 @@
+# OAuth Deep Linking
+
+## Prerequisites
+
+1. Add scheme to `app.json`:
+
+```json
+{
+ "expo": {
+ "scheme": "myapp"
+ }
+}
+```
+
+2. Install deps: `npx expo install expo-web-browser expo-auth-session`
+
+## OAuth with useSSO
+
+```tsx
+import { useSSO } from '@clerk/expo'
+import * as WebBrowser from 'expo-web-browser'
+import { useEffect } from 'react'
+
+WebBrowser.maybeCompleteAuthSession()
+
+export function SignInScreen() {
+ const { startSSOFlow } = useSSO()
+
+ const handleGoogle = async () => {
+ try {
+ const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({
+ strategy: 'oauth_google',
+ redirectUrl: 'myapp://oauth-callback',
+ })
+
+ if (createdSessionId) {
+ await setActive!({ session: createdSessionId })
+ } else if (signUp?.status === 'missing_requirements') {
+ // Handle missing fields (e.g. username)
+ }
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ return
+}
+```
+
+## Supported Strategies
+
+| Provider | Strategy |
+|----------|----------|
+| Google | `oauth_google` |
+| Apple | `oauth_apple` |
+| GitHub | `oauth_github` |
+| Microsoft | `oauth_microsoft` |
+| Facebook | `oauth_facebook` |
+
+## Callback Screen
+
+Create `app/oauth-callback.tsx`:
+
+```tsx
+import { useEffect } from 'react'
+import * as WebBrowser from 'expo-web-browser'
+
+WebBrowser.maybeCompleteAuthSession()
+
+export default function OAuthCallback() {
+ return null
+}
+```
+
+## CRITICAL
+
+- `WebBrowser.maybeCompleteAuthSession()` must be called at the TOP LEVEL of the callback screen (not inside a hook or effect)
+- `redirectUrl` must match the scheme in `app.json` exactly
+- `useSSO` replaces `useOAuth` from older `@clerk/expo` versions
diff --git a/.agents/skills/clerk-expo-patterns/references/protected-routes.md b/.agents/skills/clerk-expo-patterns/references/protected-routes.md
new file mode 100644
index 0000000..6ab9f89
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/references/protected-routes.md
@@ -0,0 +1,66 @@
+# Protected Routes (Expo Router)
+
+## Layout-Level Guard
+
+Protect an entire group in `app/(auth)/_layout.tsx`:
+
+```tsx
+import { useAuth } from '@clerk/expo'
+import { Redirect, Stack } from 'expo-router'
+
+export default function AuthLayout() {
+ const { isSignedIn, isLoaded } = useAuth()
+
+ if (!isLoaded) return null
+
+ if (!isSignedIn) return
+
+ return
+}
+```
+
+## Screen-Level Guard
+
+For a single screen:
+
+```tsx
+import { useAuth } from '@clerk/expo'
+import { Redirect } from 'expo-router'
+import { View, Text } from 'react-native'
+
+export default function ProfileScreen() {
+ const { isSignedIn, isLoaded, userId } = useAuth()
+
+ if (!isLoaded) return null
+ if (!isSignedIn) return
+
+ return (
+
+ User: {userId}
+
+ )
+}
+```
+
+## Recommended File Structure
+
+```
+app/
+├── _layout.tsx # Root layout with ClerkProvider
+├── (public)/
+│ ├── sign-in.tsx
+│ └── sign-up.tsx
+└── (protected)/
+ ├── _layout.tsx # Auth guard here
+ ├── (tabs)/
+ │ ├── _layout.tsx
+ │ ├── home.tsx
+ │ └── profile.tsx
+ └── settings.tsx
+```
+
+## CRITICAL
+
+- Always check `isLoaded` before checking `isSignedIn` — Clerk needs time to restore the session from the token cache
+- Returning `null` while loading prevents a flash of the sign-in screen
+- Use `` from `expo-router`, not `router.push` in effects, to avoid React render warnings
diff --git a/.agents/skills/clerk-expo-patterns/references/push-notifications.md b/.agents/skills/clerk-expo-patterns/references/push-notifications.md
new file mode 100644
index 0000000..5f5e313
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/references/push-notifications.md
@@ -0,0 +1,65 @@
+# Push Notifications with User Context
+
+Store the Expo push token against the Clerk user using `publicMetadata` or your own database.
+
+## Register Push Token After Sign-In
+
+```tsx
+import { useUser } from '@clerk/expo'
+import * as Notifications from 'expo-notifications'
+import { useEffect } from 'react'
+
+export function PushTokenRegistrar() {
+ const { user, isLoaded } = useUser()
+
+ useEffect(() => {
+ if (!isLoaded || !user) return
+
+ async function register() {
+ const { status } = await Notifications.requestPermissionsAsync()
+ if (status !== 'granted') return
+
+ const token = (await Notifications.getExpoPushTokenAsync()).data
+
+ await user.update({
+ unsafeMetadata: {
+ ...user.unsafeMetadata,
+ expoPushToken: token,
+ },
+ })
+ }
+
+ register()
+ }, [isLoaded, user])
+
+ return null
+}
+```
+
+> Use `unsafeMetadata` for client-writable data. Use `publicMetadata` (server-only write) for verified data.
+
+## Send Notification to User (Server)
+
+```tsx
+import { clerkClient } from '@clerk/nextjs/server'
+
+async function sendNotification(userId: string, title: string, body: string) {
+ const client = await clerkClient()
+ const user = await client.users.getUser(userId)
+ const token = user.unsafeMetadata?.expoPushToken as string | undefined
+
+ if (!token) return
+
+ await fetch('https://exp.host/--/api/v2/push/send', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ to: token, title, body }),
+ })
+}
+```
+
+## CRITICAL
+
+- `user.update()` is client-side — it writes `unsafeMetadata` without server auth
+- For verified/sensitive data, use the Clerk Backend SDK from your server to write `publicMetadata`
+- Re-register the push token if `user.id` changes (org switch does not change user.id, but sign-out/sign-in as different user does)
diff --git a/.agents/skills/clerk-expo-patterns/references/token-storage.md b/.agents/skills/clerk-expo-patterns/references/token-storage.md
new file mode 100644
index 0000000..446fa4e
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/references/token-storage.md
@@ -0,0 +1,60 @@
+# Token Storage
+
+Clerk stores the active session token in memory by default. In native apps, use a persistent token cache so sessions survive app restarts.
+
+## Built-in Cache (Recommended)
+
+```tsx
+import { tokenCache } from '@clerk/expo/token-cache'
+
+
+```
+
+Uses `expo-secure-store` with `keychainAccessible: AFTER_FIRST_UNLOCK`.
+
+Install peer dep: `npx expo install expo-secure-store`
+
+## Custom Cache
+
+Implement the `TokenCache` interface:
+
+```tsx
+import * as SecureStore from 'expo-secure-store'
+import type { TokenCache } from '@clerk/expo/token-cache'
+
+const tokenCache: TokenCache = {
+ async getToken(key: string) {
+ try {
+ return await SecureStore.getItemAsync(key, {
+ keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
+ })
+ } catch {
+ await SecureStore.deleteItemAsync(key)
+ return null
+ }
+ },
+ saveToken(key: string, token: string) {
+ return SecureStore.setItemAsync(key, token, {
+ keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
+ })
+ },
+}
+```
+
+## Expo Web
+
+`expo-secure-store` is not available on web. Provide separate implementations:
+
+```tsx
+import { Platform } from 'react-native'
+import { tokenCache as nativeCache } from '@clerk/expo/token-cache'
+
+const cache = Platform.OS === 'web' ? undefined : nativeCache
+
+```
+
+## CRITICAL
+
+- `AFTER_FIRST_UNLOCK` means the token is inaccessible until the user unlocks the device at least once after a restart
+- Always delete the item on `getToken` error — corrupted keychain entries cause silent auth failures
+- Never store tokens in AsyncStorage — it is not encrypted
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app.json b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app.json
new file mode 100644
index 0000000..493f661
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app.json
@@ -0,0 +1,8 @@
+{
+ "expo": {
+ "name": "clerk-expo",
+ "slug": "clerk-expo",
+ "scheme": "clerkexpo",
+ "plugins": ["expo-secure-store", "@clerk/expo"]
+ }
+}
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(auth)/_layout.tsx b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(auth)/_layout.tsx
new file mode 100644
index 0000000..ebf3c13
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(auth)/_layout.tsx
@@ -0,0 +1,9 @@
+import { useAuth } from '@clerk/expo'
+import { Redirect, Stack } from 'expo-router'
+
+export default function AuthRoutesLayout() {
+ const { isSignedIn, isLoaded } = useAuth()
+ if (!isLoaded) return null
+ if (isSignedIn) return
+ return
+}
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/_layout.tsx b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/_layout.tsx
new file mode 100644
index 0000000..769124f
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/_layout.tsx
@@ -0,0 +1,9 @@
+import { useAuth } from '@clerk/expo'
+import { Redirect, Stack } from 'expo-router'
+
+export default function Layout() {
+ const { isSignedIn, isLoaded } = useAuth()
+ if (!isLoaded) return null
+ if (!isSignedIn) return
+ return
+}
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/index.tsx b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/index.tsx
new file mode 100644
index 0000000..5768976
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/(home)/index.tsx
@@ -0,0 +1,32 @@
+import { Show, useUser } from '@clerk/expo'
+import { useClerk } from '@clerk/expo'
+import { Link } from 'expo-router'
+import { Text, View, Pressable, StyleSheet } from 'react-native'
+
+export default function Page() {
+ const { user } = useUser()
+ const { signOut } = useClerk()
+
+ return (
+
+ Welcome!
+
+ Sign in
+ Sign up
+
+
+ Hello {user?.id}
+ signOut()}>
+ Sign out
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, padding: 20, paddingTop: 60, gap: 16 },
+ title: { fontSize: 24, fontWeight: 'bold' },
+ button: { backgroundColor: '#0a7ea4', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, alignItems: 'center' },
+ buttonText: { color: '#fff', fontWeight: '600' },
+})
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/_layout.tsx b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/_layout.tsx
new file mode 100644
index 0000000..aca6278
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/app/_layout.tsx
@@ -0,0 +1,17 @@
+import { ClerkProvider } from '@clerk/expo'
+import { tokenCache } from '@clerk/expo/token-cache'
+import { Slot } from 'expo-router'
+
+const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
+
+if (!publishableKey) {
+ throw new Error('Add your Clerk Publishable Key to the .env file')
+}
+
+export default function RootLayout() {
+ return (
+
+
+
+ )
+}
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/package.json b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/package.json
new file mode 100644
index 0000000..397ae76
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "clerk-expo",
+ "scripts": { "start": "expo start" },
+ "dependencies": {
+ "@clerk/expo": "latest",
+ "expo": "latest",
+ "expo-router": "latest",
+ "expo-secure-store": "latest",
+ "react": "latest",
+ "react-native": "latest"
+ }
+}
diff --git a/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/tsconfig.json b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/tsconfig.json
new file mode 100644
index 0000000..a354d97
--- /dev/null
+++ b/.agents/skills/clerk-expo-patterns/templates/expo-basic-auth/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/.agents/skills/clerk-setup/SKILL.md b/.agents/skills/clerk-setup/SKILL.md
new file mode 100644
index 0000000..c5aafb0
--- /dev/null
+++ b/.agents/skills/clerk-setup/SKILL.md
@@ -0,0 +1,327 @@
+---
+name: clerk-setup
+description: Add Clerk authentication to any project by following the official quickstart
+ guides.
+license: MIT
+allowed-tools: WebFetch
+compatibility: Requires NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY (or framework-specific equivalents like VITE_CLERK_PUBLISHABLE_KEY for Vite-based apps). Keys can be auto-generated via Keyless on first SDK initialization, or pulled from the Clerk Dashboard. Requires Node.js 20.9.0 or higher.
+metadata:
+ author: clerk
+ version: 2.3.0
+---
+
+# Adding Clerk
+
+> **Version**: Check `package.json` for the SDK version — see `clerk` skill for the version table. Core 2 differences are noted inline with `> **Core 2 ONLY (skip if current SDK):**` callouts.
+
+This skill sets up Clerk for authentication by following the official quickstart documentation. For agents, the `clerk` CLI handles most of this end to end — see the next section.
+
+## Agent-first: Provision via CLI
+
+The `clerk` CLI replaces most Dashboard clicks. Three scenarios cover almost everything:
+
+### Scenario A — New project, new Clerk app
+
+```bash
+clerk init --framework -y
+```
+
+`clerk init` creates the Clerk app via PLAPI, links the project, writes the framework-specific publishable + secret keys to the right env file (e.g. `.env.local` for Next.js, `.env` for Vite-based projects), and installs the SDK package.
+
+### Scenario B — Existing project, existing Clerk app
+
+```bash
+clerk auth login # one-time OAuth (skip if already logged in)
+clerk link # autolinks if a CLERK_PUBLISHABLE_KEY is in your .env
+clerk link --app app_xxx # explicit form, required in agent mode
+clerk env pull # writes the framework-detected env vars
+```
+
+### Scenario C — Existing project, new Clerk app
+
+```bash
+clerk auth login
+clerk apps create "My App" --json # returns the new app_id
+clerk link --app app_xxx
+clerk env pull
+```
+
+### Daily ops
+
+```bash
+clerk env pull # refresh keys (uses linked profile)
+clerk env pull --instance prod # production keys
+clerk doctor --json # framework integration health check
+```
+
+### Rotate the secret key (replaces Dashboard rotation)
+
+PLAPI exposes secret-key rotation directly. Use raw `clerk api` until the friendly wrapper ships:
+
+```bash
+clerk api --platform POST /v1/platform/applications//rotate_secret_keys \
+ -d '{"delay_old_secrets_expiration_hours": 24, "reason": "scheduled rotation"}'
+```
+
+`delay_old_secrets_expiration_hours` keeps the old key valid for the grace period so deploys can roll forward without downtime.
+
+### Notes for agents
+
+- `clerk link` (no flags) only autolinks when a `CLERK_PUBLISHABLE_KEY` is already in `.env` / `.env.local`. Without it, agent mode errors out: "Cannot select an application in agent mode." When that happens, run `clerk apps list --json`, and ask the user which `app_id` to link rather than guessing.
+- Pass `--json` on `apps list/create`, `users create`, and `doctor` for parseable output.
+- The CLI auto-detects framework env var names (`VITE_CLERK_PUBLISHABLE_KEY` for Vite, `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` for Next.js, etc.) and target file (`.env.development.local` > `.env.local` > `.env`).
+
+## Quick Reference (Dashboard fallback)
+
+If the CLI isn't an option (sandboxed environments, docs walkthroughs), here's the manual Dashboard path:
+
+| Step | Action |
+|------|--------|
+| 1. Detect framework | Check `package.json` dependencies |
+| 2. Fetch quickstart | Use WebFetch on the appropriate docs URL |
+| 3. Follow instructions | Execute steps; create `proxy.ts` (Next.js <=15: `middleware.ts`) |
+| 4. Get API keys | From [dashboard.clerk.com](https://dashboard.clerk.com/~/api-keys) |
+
+> If the project has `components.json` (shadcn/ui), apply the shadcn theme after setup. See `clerk-custom-ui` skill → shadcn Theme.
+
+## Framework Detection
+
+Check `package.json` to identify the framework:
+
+| Dependency | Framework | Quickstart URL |
+|------------|-----------|----------------|
+| `next` | Next.js | `https://clerk.com/docs/nextjs/getting-started/quickstart` |
+| `@remix-run/react` | Remix (deprecated) | Migrate to React Router v7 — use the React Router quickstart below |
+| `react-router` | React Router (v7+) | `https://clerk.com/docs/react-router/getting-started/quickstart` |
+| `astro` | Astro | `https://clerk.com/docs/astro/getting-started/quickstart` |
+| `nuxt` | Nuxt | `https://clerk.com/docs/nuxt/getting-started/quickstart` |
+| `@tanstack/react-start` | TanStack Start | `https://clerk.com/docs/tanstack-react-start/getting-started/quickstart` |
+| `react` (no framework) | React SPA | `https://clerk.com/docs/react/getting-started/quickstart` |
+| `vue` | Vue | `https://clerk.com/docs/vue/getting-started/quickstart` |
+| `express` | Express | `https://clerk.com/docs/expressjs/getting-started/quickstart` |
+| `fastify` | Fastify | `https://clerk.com/docs/fastify/getting-started/quickstart` |
+| `expo` | Expo | `https://clerk.com/docs/expo/getting-started/quickstart` |
+
+For other platforms:
+- **Chrome Extension**: `https://clerk.com/docs/chrome-extension/getting-started/quickstart`
+- **Android**: `https://clerk.com/docs/android/getting-started/quickstart`
+- **iOS**: `https://clerk.com/docs/ios/getting-started/quickstart`
+- **Vanilla JavaScript**: `https://clerk.com/docs/js-frontend/getting-started/quickstart`
+
+## Decision Tree
+
+```
+User Request: "Add Clerk" / "Add authentication"
+ │
+ ├─ Read package.json
+ │
+ ├─ Existing auth detected?
+ │ ├─ YES → Audit → Migration plan
+ │ └─ NO → Fresh install
+ │
+ ├─ Identify framework → WebFetch quickstart → Follow instructions
+ │ └─ Next.js? → Create proxy.ts (Next.js <=15: middleware.ts)
+ │
+ └─ components.json exists? → YES → Apply shadcn theme (see clerk-custom-ui)
+```
+
+## Setup Process
+
+### 1. Detect the Framework
+
+Read the project's `package.json` and match dependencies to the table above.
+
+### 2. Fetch the Quickstart Guide
+
+Use WebFetch to retrieve the official quickstart for the detected framework:
+
+```
+WebFetch: https://clerk.com/docs/{framework}/getting-started/quickstart
+Prompt: "Extract the complete setup instructions including all code snippets, file paths, and configuration steps."
+```
+
+### 3. Follow the Instructions
+
+Execute each step from the quickstart guide:
+- Install the required packages
+- Set up environment variables
+- Add the provider and proxy/middleware
+- Create sign-in/sign-up routes if needed
+- Test the integration
+
+> **Next.js:** Create `proxy.ts` (Next.js <=15: `middleware.ts`). See the `clerk-nextjs-patterns` skill for middleware strategies.
+
+> **shadcn/ui detected** (`components.json` exists): ALWAYS apply the shadcn theme. See `clerk-custom-ui` skill → shadcn Theme section.
+
+### 4. Get API Keys
+
+Two paths for development API keys:
+
+**Keyless (Automatic)**
+- On first SDK initialization, Clerk auto-generates dev keys and shows a "Configure your application" button in the bottom right of the running app
+- No manual key setup required, keys are created and injected automatically
+- Selecting "Configure your application" associates the auto-generated app with your Clerk account so you can edit it from the Dashboard
+- Simplest path for new projects
+
+**Manual (Dashboard)**
+- Get keys from [dashboard.clerk.com](https://dashboard.clerk.com/~/api-keys) if Keyless doesn't trigger
+- **Publishable Key**: Starts with `pk_test_` or `pk_live_`
+- **Secret Key**: Starts with `sk_test_` or `sk_live_`
+- Set as environment variables: `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`
+
+## Migrating from Another Auth Provider
+
+If the project already has authentication, create a migration plan before replacing it.
+
+### Detect Existing Auth
+
+Check `package.json` for existing auth libraries:
+- `next-auth` / `@auth/core` → NextAuth/Auth.js
+- `@supabase/supabase-js` → Supabase Auth
+- `firebase` / `firebase-admin` → Firebase Auth
+- `@aws-amplify/auth` → AWS Cognito
+- `auth0` / `@auth0/nextjs-auth0` → Auth0
+- `passport` → Passport.js
+- Custom JWT/session implementation
+
+### Migration Process
+
+1. **Audit current auth** - Identify all auth touchpoints:
+ - Sign-in/sign-up pages
+ - Session/token handling
+ - Protected routes and middleware
+ - User data storage (database tables, external IDs)
+ - OAuth providers configured
+
+2. **Create migration plan** - Consider:
+ - **User data export** - Export users and import via Clerk's Backend API
+ - **Password hashes** - Clerk can upgrade hashes to Bcrypt transparently
+ - **External IDs** - Store legacy user IDs as `external_id` in Clerk
+ - **Session handling** - Existing sessions will terminate on switch
+
+3. **Choose migration strategy**:
+ - **Big bang** - Switch all users at once (simpler, requires maintenance window)
+ - **Trickle migration** - Run both systems temporarily (lower risk, higher complexity)
+
+### Migration Reference
+
+- **Migration Overview**: https://clerk.com/docs/guides/development/migrating/overview
+
+## SDK Notes
+
+### Package Names
+
+| Package | Install |
+|---------|---------|
+| Next.js | `@clerk/nextjs` |
+| React | `@clerk/react` |
+| Expo | `@clerk/expo` |
+| React Router | `@clerk/react-router` |
+| TanStack Start | `@clerk/tanstack-react-start` |
+
+> **Core 2 ONLY (skip if current SDK):** React and Expo packages have different names: `@clerk/clerk-react` and `@clerk/clerk-expo` (with `clerk-` prefix).
+
+### ClerkProvider Placement (Next.js)
+
+`ClerkProvider` must be placed **inside ``**, not wrapping ``:
+
+```tsx
+// root layout.tsx
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+```
+
+> **Core 2 ONLY (skip if current SDK):** `ClerkProvider` can wrap `` directly.
+
+### Dynamic Rendering (Next.js)
+
+For dynamic rendering with auth data, use the `dynamic` prop:
+
+```tsx
+{children}
+```
+
+### Node.js Requirement
+
+Requires **Node.js 20.9.0** or higher.
+
+> **Core 2 ONLY (skip if current SDK):** Minimum Node.js 18.17.0.
+
+### Themes Package
+
+Themes are installed from `@clerk/ui`:
+
+```bash
+npm install @clerk/ui
+```
+
+> **Core 2 ONLY (skip if current SDK):** Themes are from `@clerk/themes` instead of `@clerk/ui`.
+
+### shadcn Theme
+
+If the project uses shadcn/ui (check for `components.json` in the project root), apply the shadcn theme so Clerk components match the app's design system:
+
+```bash
+npm install @clerk/ui
+```
+
+```tsx
+import { shadcn } from '@clerk/ui/themes'
+
+{children}
+```
+
+Also import the shadcn CSS in your global styles:
+```css
+@import 'tailwindcss';
+@import '@clerk/ui/themes/shadcn.css';
+```
+
+> **Core 2 ONLY (skip if current SDK):** Import from `@clerk/themes` and `@clerk/themes/shadcn.css` instead.
+
+## Common Pitfalls
+
+> **Run `clerk doctor` first.** It checks framework integration, env vars, middleware presence, and SDK install status. Fixes a lot of these in one shot.
+
+| Issue | Solution |
+|-------|----------|
+| Missing `await` on `auth()` | In Next.js 15+, `auth()` is async: `const { userId } = await auth()` |
+| Exposing `CLERK_SECRET_KEY` | Never use the secret key in client code; only `NEXT_PUBLIC_*` keys are safe |
+| Missing middleware matcher | Include API routes: `matcher: ['/((?!.*\\..*|_next).*)', '/']` |
+| ClerkProvider placement | Must be inside `` in root layout (Core 2: could wrap ``) |
+| Auth routes not public | Allow `/sign-in`, `/sign-up` in middleware config |
+| Landing page requires auth | To keep "/" public, exclude it: `matcher: ['/((?!.*\\..*|_next|^/$).*)', '/api/(.*)']` |
+| Wrong import path | Server code uses `@clerk/nextjs/server`, client uses `@clerk/nextjs` |
+| Wrong package name | Use `@clerk/react` not `@clerk/clerk-react` (Core 2 naming) |
+
+## See Also
+
+- `clerk-custom-ui` - Custom sign-in/up components
+- `clerk-nextjs-patterns` - Advanced Next.js patterns
+- `clerk-react-patterns` - React SPA patterns
+- `clerk-react-router-patterns` - React Router patterns
+- `clerk-vue-patterns` - Vue patterns
+- `clerk-nuxt-patterns` - Nuxt patterns
+- `clerk-astro-patterns` - Astro patterns
+- `clerk-tanstack-patterns` - TanStack Start patterns
+- `clerk-expo-patterns` - Expo patterns
+- `clerk-chrome-extension-patterns` - Chrome Extension patterns
+- `clerk-orgs` - B2B multi-tenant organizations
+- `clerk-webhooks` - Webhook → database sync
+- `clerk-testing` - E2E testing setup
+- `clerk-swift` - Native iOS auth
+- `clerk-android` - Native Android auth
+- `clerk-backend-api` - Backend REST API explorer
+
+## Documentation
+
+- **Quickstart Overview**: https://clerk.com/docs/getting-started/quickstart/overview
+- **Migration Guide**: https://clerk.com/docs/guides/development/migrating/overview
+- **Full Documentation**: https://clerk.com/docs
diff --git a/.agents/skills/clerk-setup/evals/evals.json b/.agents/skills/clerk-setup/evals/evals.json
new file mode 100644
index 0000000..c9bd338
--- /dev/null
+++ b/.agents/skills/clerk-setup/evals/evals.json
@@ -0,0 +1,74 @@
+{
+ "skill_name": "clerk-setup",
+ "evals": [
+ {
+ "id": 1,
+ "prompt": "hey i just created a new next.js app with create-next-app and i want to add auth. i heard clerk is good, can you set it up? im using the app router btw and tailwind. project is at ./my-saas-app",
+ "expected_output": "Installs @clerk/nextjs, wraps layout with ClerkProvider inside , creates middleware.ts with clerkMiddleware, sets up .env.local with publishable and secret keys, detects shadcn if present",
+ "files": [],
+ "expectations": [
+ "Installs @clerk/nextjs via npm/pnpm/bun",
+ "Adds ClerkProvider to root layout.tsx inside tag, not wrapping ",
+ "Creates middleware.ts (or proxy.ts for Next.js 16+) with clerkMiddleware export",
+ "Creates or updates .env.local with NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY",
+ "Does NOT install @clerk/clerk-react or any non-Next.js Clerk package",
+ "Mentions getting API keys from dashboard.clerk.com or keyless dev mode"
+ ]
+ },
+ {
+ "id": 2,
+ "prompt": "im building a rest api with express and typescript, no frontend. i need to protect some routes with authentication. can you add clerk? i want /api/public to be open and everything under /api/private/* to require auth",
+ "expected_output": "Installs @clerk/express, adds clerkMiddleware globally, uses requireAuth for protected routes, keeps public routes open",
+ "files": [],
+ "expectations": [
+ "Installs @clerk/express (not @clerk/nextjs)",
+ "Adds clerkMiddleware() to the Express app",
+ "Shows how to protect /api/private/* routes with requireAuth or getAuth check",
+ "Leaves /api/public accessible without auth",
+ "Does NOT create React components, ClerkProvider, or any frontend code",
+ "Sets up CLERK_SECRET_KEY (and optionally CLERK_PUBLISHABLE_KEY) env vars"
+ ]
+ },
+ {
+ "id": 3,
+ "prompt": "we have a next.js app thats been using next-auth with google and github oauth for about a year. we want to switch to clerk because next-auth sessions keep expiring randomly. theres about 2000 users in our postgres db. how do we migrate without losing everyone?",
+ "expected_output": "Migration plan: audit current NextAuth setup, export users via Clerk Backend API, handle OAuth provider mapping, plan session transition",
+ "files": [],
+ "expectations": [
+ "Detects or acknowledges existing NextAuth/next-auth setup",
+ "Addresses user data migration (2000 users) - mentions Clerk Backend API import or trickle migration",
+ "Addresses OAuth provider continuity (Google and GitHub)",
+ "Mentions that existing sessions will terminate during switch",
+ "Suggests either big-bang or gradual migration strategy",
+ "Does NOT just install Clerk and ignore the existing auth system"
+ ]
+ },
+ {
+ "id": 4,
+ "prompt": "setting up a react spa with vite for an internal tool at work. no ssr, no next.js. just plain react 19 + react-router. need login/logout. whats the quickest way to get clerk working?",
+ "expected_output": "Installs @clerk/react, wraps app with ClerkProvider, no middleware needed for SPA",
+ "files": [],
+ "expectations": [
+ "Installs @clerk/react (not @clerk/nextjs or @clerk/clerk-react)",
+ "Wraps app root with ClerkProvider and passes publishableKey",
+ "Does NOT create middleware.ts or proxy.ts (not applicable to SPAs)",
+ "Does NOT reference Next.js-specific patterns like server components or auth()",
+ "Shows SignIn/SignUp components or redirect-based auth flow",
+ "Sets up NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY or VITE_CLERK_PUBLISHABLE_KEY env var"
+ ]
+ },
+ {
+ "id": 5,
+ "prompt": "i have an astro site with some react islands, its mostly static but i want to add a members-only section. never used clerk before. using bun as my package manager",
+ "expected_output": "Installs @clerk/astro via bun, configures Astro integration, sets up protected routes for members-only section",
+ "files": [],
+ "expectations": [
+ "Installs @clerk/astro using bun (not npm or pnpm)",
+ "Configures Clerk as an Astro integration in astro.config",
+ "Shows how to protect specific routes for the members-only section",
+ "Sets up environment variables for Clerk keys",
+ "Does NOT install @clerk/nextjs or create Next.js-specific files"
+ ]
+ }
+ ]
+}
diff --git a/.agents/skills/clerk-webhooks/SKILL.md b/.agents/skills/clerk-webhooks/SKILL.md
new file mode 100644
index 0000000..fe12b04
--- /dev/null
+++ b/.agents/skills/clerk-webhooks/SKILL.md
@@ -0,0 +1,345 @@
+---
+name: clerk-webhooks
+description: Clerk webhooks for real-time events and data syncing. Verify with verifyWebhook
+ from the framework-specific package. Handle user, session, organization, billing, and
+ payment events. Build event-driven features like database sync, notifications, and
+ integrations.
+allowed-tools: WebFetch
+license: MIT
+metadata:
+ author: clerk
+ version: 1.2.0
+compatibility: Requires CLERK_WEBHOOK_SIGNING_SECRET (svix signing secret from Clerk dashboard)
+---
+
+# Webhooks
+
+Output complete, working webhook handlers with `verifyWebhook(req)` verification in every handler.
+
+## When to Use Webhooks
+
+Webhooks are **asynchronous and eventually consistent**. Delivery is fast but not guaranteed to be immediate, and may occasionally fail (Svix retries on a fixed schedule). Use them for:
+
+- Database sync (a separate users / orgs table that follows Clerk)
+- Notifications (welcome emails, Slack pings, internal alerts)
+- Integrations triggered by lifecycle events
+
+Do NOT rely on webhook delivery as part of a synchronous flow such as onboarding ("user signs up, then we read X from our DB"). For data the user just created, read it from the [Clerk session token](https://clerk.com/docs/guides/sessions/session-tokens) or call the Backend API directly. Webhooks fill the gap when you need data about *other* users or events the session token doesn't carry.
+
+## Verify Every Webhook
+
+Use `verifyWebhook(req)` from the framework-specific package (`@clerk/nextjs/webhooks`, `@clerk/express/webhooks`, etc.). It reads `CLERK_WEBHOOK_SIGNING_SECRET` automatically and throws on bad signatures. Skipping verification, even for notification-only handlers, exposes the endpoint to spoofed events.
+
+## Make the Webhook Route Public
+
+Webhook routes must be excluded from Clerk middleware protection. Without this, Clerk returns 401.
+
+```typescript
+// proxy.ts (Next.js <=15: middleware.ts)
+import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
+
+const isPublicRoute = createRouteMatcher(['/api/webhooks(.*)'])
+
+export default clerkMiddleware(async (auth, req) => {
+ if (!isPublicRoute(req)) await auth.protect()
+})
+```
+
+## Complete Webhook Handler (Next.js App Router)
+
+```typescript
+// app/api/webhooks/route.ts
+import { verifyWebhook } from '@clerk/nextjs/webhooks'
+import { NextRequest } from 'next/server'
+import { db } from '@/lib/db'
+
+export async function POST(req: NextRequest) {
+ // ALWAYS verify - never skip, even for notification-only handlers
+ let evt
+ try {
+ evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET automatically
+ } catch (err) {
+ console.error('Webhook verification failed:', err)
+ return new Response('Verification failed', { status: 400 })
+ }
+
+ if (evt.type === 'user.created') {
+ const { id, email_addresses, first_name, last_name } = evt.data
+ const email = email_addresses[0]?.email_address
+ const name = `${first_name ?? ''} ${last_name ?? ''}`.trim()
+ await db.users.create({ data: { clerkId: id, email, name } })
+ }
+
+ if (evt.type === 'user.updated') {
+ const { id, email_addresses, first_name, last_name } = evt.data
+ const email = email_addresses[0]?.email_address
+ await db.users.update({ where: { clerkId: id }, data: { email, first_name, last_name } })
+ }
+
+ if (evt.type === 'user.deleted') {
+ const { id } = evt.data
+ await db.users.delete({ where: { clerkId: id } })
+ }
+
+ if (evt.type === 'organizationMembership.created') {
+ const { organization, public_user_data, role } = evt.data
+ const orgId = organization.id
+ const userId = public_user_data.user_id
+ await db.teamMembers.create({ data: { orgId, userId, role } })
+ }
+
+ if (evt.type === 'organizationMembership.deleted') {
+ const { organization, public_user_data } = evt.data
+ const orgId = organization.id
+ const userId = public_user_data.user_id
+ await db.teamMembers.delete({ where: { orgId_userId: { orgId, userId } } })
+ }
+
+ return new Response('OK', { status: 200 })
+}
+```
+
+## Full Example: Welcome Email (Resend) + Slack Notification on user.created
+
+Notification-only handlers still verify the signature. Same pattern as the database-sync handler:
+
+```typescript
+// app/api/webhooks/route.ts
+import { verifyWebhook } from '@clerk/nextjs/webhooks'
+import { NextRequest } from 'next/server'
+import { Resend } from 'resend'
+
+const resend = new Resend(process.env.RESEND_API_KEY)
+
+export async function POST(req: NextRequest) {
+ // Step 1: ALWAYS verify the webhook signature - NEVER skip this
+ let evt
+ try {
+ evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET env var
+ } catch (err) {
+ console.error('Webhook verification failed:', err)
+ return new Response('Verification failed', { status: 400 })
+ }
+
+ // Step 2: Listen for user.created event
+ if (evt.type === 'user.created') {
+ // Step 3: Extract user email and name from webhook payload
+ const { id, email_addresses, first_name, last_name } = evt.data
+ const email = email_addresses[0]?.email_address
+ const name = `${first_name ?? ''} ${last_name ?? ''}`.trim()
+
+ // Step 4: Call Resend API to send welcome email
+ await resend.emails.send({
+ from: 'noreply@yourdomain.com',
+ to: email,
+ subject: 'Welcome!',
+ html: `Hi ${name}, welcome to our app!
`,
+ })
+
+ // Step 5: Post notification to Slack channel
+ await fetch(process.env.SLACK_WEBHOOK_URL!, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: `New user signed up: ${name} (${email})`,
+ }),
+ })
+ }
+
+ // Always return 200 to acknowledge receipt
+ return new Response('OK', { status: 200 })
+}
+```
+
+**Also include proxy.ts (Next.js <=15: middleware.ts) to make the route public:**
+```typescript
+// proxy.ts (Next.js <=15: middleware.ts)
+import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
+const isPublicRoute = createRouteMatcher(['/api/webhooks(.*)'])
+export default clerkMiddleware(async (auth, req) => {
+ if (!isPublicRoute(req)) await auth.protect()
+})
+```
+
+## Full Example: Organization Membership Sync to Database
+
+```typescript
+// app/api/webhooks/route.ts
+import { verifyWebhook } from '@clerk/nextjs/webhooks'
+import { NextRequest } from 'next/server'
+import { db } from '@/lib/db' // your database client
+
+export async function POST(req: NextRequest) {
+ // ALWAYS verify signature - never skip, even for simple handlers
+ let evt
+ try {
+ evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET env var
+ } catch (err) {
+ console.error('Webhook verification failed:', err)
+ return new Response('Verification failed', { status: 400 })
+ }
+
+ if (evt.type === 'organization.created') {
+ const { id, name } = evt.data
+ await db.workspaces.create({
+ data: { orgId: id, name, createdAt: new Date() },
+ })
+ }
+
+ if (evt.type === 'organizationMembership.created') {
+ // Extract organization ID, user ID, and role from payload
+ const { organization, public_user_data, role } = evt.data
+ const orgId = organization.id
+ const userId = public_user_data.user_id
+
+ // Add to team_members table
+ await db.team_members.create({
+ data: { orgId, userId, role },
+ })
+
+ // Create workspace record for new member
+ await db.workspaces.create({
+ data: { orgId, userId, createdAt: new Date() },
+ })
+ }
+
+ if (evt.type === 'organizationMembership.deleted') {
+ // Extract organization ID and user ID from payload
+ const { organization, public_user_data } = evt.data
+ const orgId = organization.id
+ const userId = public_user_data.user_id
+
+ // Remove from team_members table
+ await db.team_members.delete({
+ where: { orgId, userId },
+ })
+
+ // Remove workspace record
+ await db.workspaces.deleteMany({
+ where: { orgId, userId },
+ })
+ }
+
+ // Return 200 status on success
+ return new Response('OK', { status: 200 })
+}
+```
+
+## Other Frameworks
+
+For Express, Astro, Fastify, Nuxt, React Router, and TanStack Start, use the framework-specific `verifyWebhook` adapter. Each Clerk SDK package ships its own (`@clerk/express/webhooks`, `@clerk/astro/webhooks`, `@clerk/fastify/webhooks`, etc.).
+
+See `references/frameworks.md` for full handler examples per framework.
+
+## Type Narrowing for `evt.data`
+
+`verifyWebhook` returns `WebhookEvent`, a discriminated union of all event types. Narrow with `evt.type` to get type-safe access to `evt.data`:
+
+```typescript
+const evt = await verifyWebhook(req)
+
+if (evt.type === 'user.created') {
+ // evt.data is now UserJSON, autocompletes id, email_addresses, etc.
+ console.log(evt.data.id)
+}
+```
+
+For manual typing of nested payloads, import the JSON types from your framework's webhook subpath: `DeletedObjectJSON`, `EmailJSON`, `OrganizationInvitationJSON`, `OrganizationJSON`, `OrganizationMembershipJSON`, `SessionJSON`, `SMSMessageJSON`, `UserJSON`.
+
+## Payload Field Reference
+
+### User events (`user.created`, `user.updated`, `user.deleted`)
+```typescript
+const {
+ id, // Clerk user ID
+ email_addresses, // array; [0].email_address is primary email
+ first_name,
+ last_name,
+ image_url,
+ public_metadata,
+} = evt.data
+```
+
+### Organization events (`organization.created`, `organization.updated`, `organization.deleted`)
+```typescript
+const {
+ id, // org ID
+ name, // org name
+ slug,
+} = evt.data
+```
+
+### Organization Membership events (`organizationMembership.created`, `organizationMembership.updated`, `organizationMembership.deleted`)
+```typescript
+const {
+ organization, // { id, name, ... }
+ public_user_data, // { user_id, first_name, last_name, ... }
+ role, // e.g. 'org:admin', 'org:member'
+} = evt.data
+// Access: organization.id, public_user_data.user_id, role
+```
+
+## Supported Events (Full Catalog)
+
+**User**: `user.created` `user.updated` `user.deleted`
+
+**Session**: `session.created` `session.ended` `session.removed` `session.revoked`
+
+**Organization**: `organization.created` `organization.updated` `organization.deleted`
+
+**Organization Membership**: `organizationMembership.created` `organizationMembership.updated` `organizationMembership.deleted`
+
+**Organization Domain**: `organizationDomain.created` `organizationDomain.updated` `organizationDomain.deleted`
+
+**Organization Invitation**: `organizationInvitation.accepted` `organizationInvitation.created` `organizationInvitation.revoked`
+
+**Communication**: `email.created` `sms.created`
+
+**Waitlist**: `waitlistEntry.created` `waitlistEntry.updated`
+
+**Permission**: `permission.created` `permission.updated` `permission.deleted`
+
+**Role**: `role.created` `role.updated` `role.deleted`
+
+**Subscription**: `subscription.created` `subscription.updated` `subscription.active` `subscription.pastDue`
+
+**Subscription Item**: `subscriptionItem.created` `subscriptionItem.active` `subscriptionItem.updated` `subscriptionItem.canceled` `subscriptionItem.upcoming` `subscriptionItem.ended` `subscriptionItem.abandoned` `subscriptionItem.incomplete` `subscriptionItem.pastDue` `subscriptionItem.freeTrialEnding`
+
+**Payment**: `paymentAttempt.created` `paymentAttempt.updated`
+
+## Webhook Reliability
+
+**Retries**: Svix retries failed webhooks on a set schedule (see [Svix Retry Schedule](https://docs.svix.com/retries)). Return 2xx to succeed, 4xx/5xx to retry. Use the `svix-id` header as an idempotency key to deduplicate retried events.
+
+**Replay**: Failed webhooks can be replayed from Dashboard.
+
+## Common Pitfalls
+
+| Symptom | Cause | Fix |
+|---------|-------|-----|
+| Verification fails (Next.js) | Wrong import or usage | Use `@clerk/nextjs/webhooks`, pass `req` directly |
+| Verification fails (Express) | Using `express.json()` | Use `express.raw({ type: 'application/json' })` for webhook route |
+| Route not found (404) | Wrong path | Use `/api/webhooks` or preserve existing path |
+| Not authorized (401) | Route is protected by middleware | Make route public in `clerkMiddleware()` |
+| No data in DB | Async job pending | Wait/check logs |
+| Duplicate entries | Only handling `user.created` | Also handle `user.updated` |
+| Timeouts | Handler too slow | Queue async work, return 200 first |
+
+## Testing & Deployment
+
+**Local**: Tunnel `localhost:3000` to the internet so Clerk can reach the endpoint. Common options: `ngrok`, `localtunnel`, `Cloudflare Tunnel`. Add the public URL to the Dashboard endpoint.
+
+**Production**: Update webhook endpoint URL to production domain. Copy `CLERK_WEBHOOK_SIGNING_SECRET` to production env vars.
+
+## References
+
+| Reference | Description |
+|-----------|-------------|
+| `references/frameworks.md` | Webhook handler examples for Express, Astro, Fastify, Nuxt, React Router, TanStack Start |
+
+## See Also
+
+- `clerk-setup` - Initial Clerk install
+- `clerk-orgs` - Org membership events
+- `clerk-billing` - Subscription, subscription item, and payment attempt events
+- `clerk-backend-api` - Sync via direct API calls
\ No newline at end of file
diff --git a/.agents/skills/clerk-webhooks/evals/evals.json b/.agents/skills/clerk-webhooks/evals/evals.json
new file mode 100644
index 0000000..67100ca
--- /dev/null
+++ b/.agents/skills/clerk-webhooks/evals/evals.json
@@ -0,0 +1,61 @@
+{
+ "skill_name": "clerk-webhooks",
+ "evals": [
+ {
+ "id": 1,
+ "prompt": "every time a user signs up on our app i need to create a row in our users table in postgres (we use prisma). the table has id, email, name, clerkId, createdAt columns. can you set up the webhook handler? we're on next.js app router",
+ "expected_output": "Creates webhook route handler at app/api/webhooks/route.ts, verifies webhook signature, handles user.created event, inserts into Prisma users table",
+ "files": [],
+ "assertions": [
+ "Creates route handler at app/api/webhooks/route.ts or similar path",
+ "Uses POST method handler (not GET)",
+ "Uses verifyWebhook from @clerk/nextjs/webhooks for signature verification",
+ "Handles user.created event type and extracts email, name, id from payload",
+ "Inserts into database using Prisma with the correct column mapping (clerkId = evt.data.id)",
+ "Mentions that webhook route must be excluded from Clerk middleware (public route)"
+ ]
+ },
+ {
+ "id": 2,
+ "prompt": "i want to send a welcome email via resend when someone creates an account. also want to notify our #new-users slack channel. we already have resend and slack sdks installed",
+ "expected_output": "Webhook handler for user.created that sends welcome email via Resend and posts to Slack",
+ "files": [],
+ "assertions": [
+ "Listens for user.created webhook event",
+ "Extracts user email and name from webhook payload",
+ "Calls Resend API to send welcome email",
+ "Posts notification to Slack channel",
+ "Includes webhook signature verification",
+ "Does NOT skip verification even though it's a notification-only handler"
+ ]
+ },
+ {
+ "id": 3,
+ "prompt": "we're getting 401 errors on our webhook endpoint. the clerk dashboard shows the webhooks are being sent but our app keeps rejecting them. we're using express not next.js. heres our current code:\n\napp.post('/webhooks/clerk', express.json(), (req, res) => {\n const evt = req.body;\n console.log(evt);\n res.json({ received: true });\n});",
+ "expected_output": "Identifies missing webhook verification, fixes express body parsing (needs raw body), adds verifyWebhook from @clerk/express/webhooks",
+ "files": [],
+ "assertions": [
+ "Identifies that express.json() is wrong - needs raw body for signature verification",
+ "Shows express.raw({ type: 'application/json' }) instead of express.json() for the webhook route",
+ "Uses verifyWebhook from @clerk/express/webhooks (not raw svix Webhook class)",
+ "Preserves the existing route path /webhooks/clerk",
+ "Mentions CLERK_WEBHOOK_SIGNING_SECRET environment variable (not legacy CLERK_WEBHOOK_SECRET)",
+ "Returns 200 status on success (not just res.json)"
+ ]
+ },
+ {
+ "id": 4,
+ "prompt": "our app has clerk organizations and we need to sync org membership changes to our database. when someone joins or leaves an org we need to update our team_members table. also when an org is created we need to create a workspace record",
+ "expected_output": "Webhook handler for organizationMembership and organization events with database sync",
+ "files": [],
+ "assertions": [
+ "Handles organizationMembership.created event (user joins org)",
+ "Handles organizationMembership.deleted event (user leaves org)",
+ "Handles organization.created event (new org created)",
+ "Extracts organization ID, user ID, and role from webhook payloads",
+ "Includes webhook signature verification",
+ "Shows database operations for both team_members and workspace tables"
+ ]
+ }
+ ]
+}
diff --git a/.agents/skills/clerk-webhooks/references/frameworks.md b/.agents/skills/clerk-webhooks/references/frameworks.md
new file mode 100644
index 0000000..8d719b0
--- /dev/null
+++ b/.agents/skills/clerk-webhooks/references/frameworks.md
@@ -0,0 +1,216 @@
+# Framework-Specific Webhook Handlers
+
+Each Clerk SDK package ships its own `verifyWebhook` adapter that reads the framework's native request type and uses `CLERK_WEBHOOK_SIGNING_SECRET` automatically. Use the framework-specific import; do not roll your own with raw `svix`.
+
+Same `WebhookEvent` payload shape across all frameworks. See SKILL.md for payload field reference and the full event catalog.
+
+## Express
+
+```typescript
+import { verifyWebhook } from '@clerk/express/webhooks'
+import express from 'express'
+
+const app = express()
+
+// Use express.raw() not express.json() for the webhook route -
+// signature verification requires the raw body bytes.
+app.post('/api/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
+ try {
+ const evt = await verifyWebhook(req)
+
+ if (evt.type === 'user.created') {
+ const { id, email_addresses, first_name, last_name } = evt.data
+ const email = email_addresses[0]?.email_address
+ console.log(`New user: ${first_name} ${last_name} (${email})`)
+ }
+
+ return res.send('Webhook received')
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ return res.status(400).send('Error verifying webhook')
+ }
+})
+```
+
+## Astro
+
+```typescript
+// src/pages/api/webhooks.ts
+import { verifyWebhook } from '@clerk/astro/webhooks'
+import type { APIRoute } from 'astro'
+
+export const POST: APIRoute = async ({ request }) => {
+ try {
+ const evt = await verifyWebhook(request, {
+ signingSecret: import.meta.env.CLERK_WEBHOOK_SIGNING_SECRET,
+ })
+
+ if (evt.type === 'user.created') {
+ const { id, email_addresses } = evt.data
+ const email = email_addresses[0]?.email_address
+ console.log(`New user: ${id} (${email})`)
+ }
+
+ return new Response('Webhook received', { status: 200 })
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ return new Response('Error verifying webhook', { status: 400 })
+ }
+}
+```
+
+Astro requires explicit `signingSecret` since `import.meta.env` is not auto-read.
+
+## Fastify
+
+```typescript
+import { verifyWebhook } from '@clerk/fastify/webhooks'
+import Fastify from 'fastify'
+
+const fastify = Fastify()
+
+fastify.post('/api/webhooks', async (request, reply) => {
+ try {
+ const evt = await verifyWebhook(request)
+
+ if (evt.type === 'user.created') {
+ const { id } = evt.data
+ console.log(`New user: ${id}`)
+ }
+
+ return 'Webhook received'
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ return reply.code(400).send('Error verifying webhook')
+ }
+})
+```
+
+## Nuxt
+
+```typescript
+// server/api/webhooks.post.ts
+import { verifyWebhook } from '@clerk/nuxt/webhooks'
+
+export default defineEventHandler(async (event) => {
+ try {
+ const evt = await verifyWebhook(event)
+
+ if (evt.type === 'user.created') {
+ const { id } = evt.data
+ console.log(`New user: ${id}`)
+ }
+
+ return 'Webhook received'
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ setResponseStatus(event, 400)
+ return 'Error verifying webhook'
+ }
+})
+```
+
+Prefix the env var with `NUXT_` (i.e. `NUXT_CLERK_WEBHOOK_SIGNING_SECRET`) per Nuxt runtime config rules.
+
+When tunneling via ngrok in dev, allow the host in `nuxt.config.ts`:
+
+```typescript
+export default defineNuxtConfig({
+ vite: {
+ server: {
+ allowedHosts: ['fawn-two-nominally.ngrok-free.app'],
+ },
+ },
+})
+```
+
+## React Router
+
+```typescript
+// app/routes/webhooks.ts
+import { verifyWebhook } from '@clerk/react-router/webhooks'
+import type { Route } from './+types/webhooks'
+
+export const action = async ({ request }: Route.ActionArgs) => {
+ try {
+ const evt = await verifyWebhook(request)
+
+ if (evt.type === 'user.created') {
+ const { id } = evt.data
+ console.log(`New user: ${id}`)
+ }
+
+ return new Response('Webhook received', { status: 200 })
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ return new Response('Error verifying webhook', { status: 400 })
+ }
+}
+```
+
+Register the route in `router.ts`:
+
+```typescript
+import { type RouteConfig, route, index } from '@react-router/dev/routes'
+
+export default [
+ index('routes/home.tsx'),
+ route('api/webhooks', 'routes/webhooks.ts'),
+] satisfies RouteConfig
+```
+
+When tunneling via ngrok in dev, allow the host in `vite.config.ts`:
+
+```typescript
+export default defineConfig({
+ server: {
+ allowedHosts: ['fawn-two-nominally.ngrok-free.app'],
+ },
+})
+```
+
+## TanStack Start
+
+```typescript
+// app/routes/api/webhooks.ts
+import { verifyWebhook } from '@clerk/tanstack-react-start/webhooks'
+import { createServerFileRoute } from '@tanstack/react-start/server'
+
+export const ServerRoute = createServerFileRoute().methods({
+ POST: async ({ request }) => {
+ try {
+ const evt = await verifyWebhook(request)
+
+ if (evt.type === 'user.created') {
+ const { id } = evt.data
+ console.log(`New user: ${id}`)
+ }
+
+ return new Response('Webhook received', { status: 200 })
+ } catch (err) {
+ console.error('Error verifying webhook:', err)
+ return new Response('Error verifying webhook', { status: 400 })
+ }
+ },
+})
+```
+
+When tunneling via ngrok in dev, allow the host in `app.config.ts`:
+
+```typescript
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ server: {
+ allowedHosts: ['fawn-two-nominally.ngrok-free.app'],
+ },
+})
+```
+
+## Common Patterns Across Frameworks
+
+- All `verifyWebhook` adapters return the same `WebhookEvent` discriminated union, so handler logic (`if (evt.type === ...)`) is identical.
+- All adapters read `CLERK_WEBHOOK_SIGNING_SECRET` automatically except Astro (pass `signingSecret` option).
+- All adapters require a public webhook route, exclude `/api/webhooks(.*)` from middleware protection.
+- Vite-based frameworks (Nuxt, React Router, TanStack Start) need `allowedHosts` configured when tunneling localhost via ngrok in development.
+- Express specifically needs `express.raw({ type: 'application/json' })` for the webhook route, raw body bytes are required for signature verification.
diff --git a/.agents/skills/clerk/SKILL.md b/.agents/skills/clerk/SKILL.md
new file mode 100644
index 0000000..e3683d5
--- /dev/null
+++ b/.agents/skills/clerk/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: clerk
+description: Clerk authentication router. Use when user asks about Clerk CLI operations,
+ adding authentication, setting up Clerk, custom sign-in flows, Swift or native iOS
+ auth, native Android auth, Next.js patterns, React patterns, Vue patterns, Nuxt
+ patterns, Astro patterns, TanStack Start patterns, Expo patterns, React Router
+ patterns, Chrome Extension patterns, organizations, billing, subscriptions, payments,
+ pricing, plans, seat-based pricing, feature entitlements, syncing users, or testing.
+ Automatically routes to the specific skill based on their task.
+license: MIT
+metadata:
+ version: 2.0.0
+---
+
+# Clerk Skills Router
+
+## Version Detection
+
+Check `package.json` to determine the Clerk SDK version. This determines which patterns to use:
+
+| Package | Core 2 (LTS until Jan 2027) | Current |
+|---------|----------------------------|---------|
+| `@clerk/nextjs` | v5–v6 | v7+ |
+| `@clerk/react` or `@clerk/clerk-react` | v5–v6 | v7+ |
+| `@clerk/expo` or `@clerk/clerk-expo` | v1–v2 | v3+ |
+| `@clerk/react-router` | v1–v2 | v3+ |
+| `@clerk/tanstack-react-start` | < v0.26.0 | v0.26.0+ |
+
+**Default to current** if the version is unclear or the project is new. Core 2 packages use `@clerk/clerk-react` and `@clerk/clerk-expo` (with `clerk-` prefix); current packages use `@clerk/react` and `@clerk/expo`.
+
+All skills are written for the current SDK. When something differs in Core 2, it's noted inline with `> **Core 2 ONLY (skip if current SDK):**` callouts. The exception is `clerk-custom-ui`, which has separate `core-2/` and `core-3/` directories for custom flow hooks since those APIs are entirely different between versions.
+
+---
+
+## By Task
+
+**Adding Clerk to your project** → Use `clerk-setup`
+- Framework detection and quickstart
+- Environment setup, API keys, Keyless flow
+- Migration from other auth providers
+
+**Operating Clerk from the CLI** → Use `clerk-cli`
+- Auth, linking, `doctor`, and environment pulls
+- User, org, session, app, and instance management
+- Backend and Platform API calls through `clerk api`
+- Deploy handoff and deploy status verification
+
+**Custom sign-in/sign-up UI** → Use `clerk-custom-ui`
+- Custom authentication flows with `useSignIn` / `useSignUp` hooks
+- Appearance and styling (themes, colors, layout)
+- `` component for conditional rendering
+
+**Advanced Next.js patterns** → Use `clerk-nextjs-patterns`
+- Server vs Client auth APIs
+- Middleware strategies
+- Server Actions, caching
+- API route protection
+
+**React patterns** → Use `clerk-react-patterns`
+- Hooks (`useAuth`, `useUser`, `useClerk`)
+- Protected routes, auth guards
+- Router integration
+
+**React Router patterns** → Use `clerk-react-router-patterns`
+- Loaders & actions with auth
+- Route protection
+- SSR auth
+
+**Vue patterns** → Use `clerk-vue-patterns`
+- Composables (`useAuth`, `useUser`, `useClerk`)
+- Vue Router guards
+- Pinia auth store integration
+
+**Nuxt patterns** → Use `clerk-nuxt-patterns`
+- Server middleware auth
+- SSR auth with composables
+- Server API routes
+
+**Astro patterns** → Use `clerk-astro-patterns`
+- SSR auth pages
+- Island components with React
+- Middleware & API routes
+
+**TanStack Start patterns** → Use `clerk-tanstack-patterns`
+- Server functions with auth
+- Route protection via loaders
+- Vinxi server integration
+
+**Expo patterns** → Use `clerk-expo-patterns`
+- Secure token storage
+- OAuth deep linking
+- Push notifications with auth
+
+**Chrome Extension patterns** → Use `clerk-chrome-extension-patterns`
+- Background scripts auth
+- Popup auth flows
+- Content scripts with sync host
+
+**B2B / Organizations** → Use `clerk-orgs`
+- Multi-tenant apps
+- Organization slugs in URLs
+- Roles, permissions, RBAC
+- Member management
+
+**Billing & Subscriptions** → Use `clerk-billing`
+- ` ` component
+- Plan and feature gating with `has()`
+- Seat-based B2B billing with organizations
+- Subscription lifecycle webhooks
+- Free trials, invoicing
+
+**Webhooks** → Use `clerk-webhooks`
+- Real-time events
+- Data syncing
+- Notifications & integrations
+
+**E2E Testing** → Use `clerk-testing`
+- Playwright/Cypress setup
+- Auth flow testing
+- Test utilities
+
+**Swift / native iOS auth** → Use `clerk-swift`
+- Native iOS Swift and SwiftUI projects
+- ClerkKit and ClerkKitUI implementation guidance
+- Source-driven patterns from `clerk-ios`
+
+**Android / native mobile auth** → Use `clerk-android`
+- Native Android Kotlin and Jetpack Compose projects
+- `clerk-android-api` and `clerk-android-ui` implementation guidance
+- Source-driven patterns from `clerk-android`
+- Do not use for Expo or React Native projects
+
+**Backend REST API** → Use `clerk-backend-api`
+- Browse API tags and endpoints
+- Inspect endpoint schemas
+- Execute API requests with scope enforcement
+
+## Quick Navigation
+
+If you know your task, you can directly access:
+- `/clerk-setup` - Framework setup
+- `/clerk-cli` - CLI operations and Clerk resource management
+- `/clerk-custom-ui` - Custom flows & appearance
+- `/clerk-nextjs-patterns` - Next.js patterns
+- `/clerk-react-patterns` - React patterns
+- `/clerk-react-router-patterns` - React Router patterns
+- `/clerk-vue-patterns` - Vue patterns
+- `/clerk-nuxt-patterns` - Nuxt patterns
+- `/clerk-astro-patterns` - Astro patterns
+- `/clerk-tanstack-patterns` - TanStack Start patterns
+- `/clerk-expo-patterns` - Expo patterns
+- `/clerk-chrome-extension-patterns` - Chrome Extension patterns
+- `/clerk-orgs` - Organizations
+- `/clerk-billing` - Billing & subscriptions
+- `/clerk-webhooks` - Webhooks
+- `/clerk-testing` - Testing
+- `/clerk-swift` - Swift/native iOS
+- `/clerk-android` - Native Android
+- `/clerk-backend-api` - Backend REST API
+
+Or describe what you need and I'll recommend the right one.
diff --git a/.env.example b/.env.example
index 4ed3207..e8cb4ab 100644
--- a/.env.example
+++ b/.env.example
@@ -9,3 +9,6 @@ EXPO_PUBLIC_API_PORT=3002
#
# If unset in dev, the app uses the same IP as `npx expo start` (no hardcoded LAN IP).
# EXPO_PUBLIC_API_URL=https://your-api.example.com
+
+# Clerk publishable key (dashboard.clerk.com → API keys)
+# EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
diff --git a/app.json b/app.json
index 46c2465..ef655be 100644
--- a/app.json
+++ b/app.json
@@ -41,7 +41,9 @@
],
"expo-asset",
"expo-font",
- "expo-video"
+ "expo-video",
+ "@clerk/expo",
+ "expo-secure-store"
],
"experiments": {
"typedRoutes": true,
diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx
index d41a490..1cf5270 100644
--- a/app/(auth)/_layout.tsx
+++ b/app/(auth)/_layout.tsx
@@ -1,6 +1,17 @@
-import { Stack } from "expo-router";
+import { useAuth } from "@clerk/expo";
+import { Redirect, Stack } from "expo-router";
export default function AuthLayout() {
+ const { isSignedIn, isLoaded } = useAuth();
+
+ if (!isLoaded) {
+ return null;
+ }
+
+ if (isSignedIn) {
+ return ;
+ }
+
return (
;
+ }
+
return (
s.countries);
const selectedRegion = useCultureFeedStore((s) => s.selectedRegion);
const hasLoadedOnce = useCultureFeedStore((s) => s.hasLoadedOnce);
@@ -51,10 +50,7 @@ export default function CultureScreen() {
}}
/>
) : showLoading ? (
-
-
- Loading culture clips…
-
+
) : showEmpty ? (
{
@@ -77,15 +73,4 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: "#0b132b",
},
- loading: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- gap: 16,
- },
- loadingText: {
- fontSize: 14,
- fontFamily: "Poppins-Regular",
- color: "rgba(255, 255, 255, 0.8)",
- },
});
diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx
index 03df8dc..c59d509 100644
--- a/app/(tabs)/explore.tsx
+++ b/app/(tabs)/explore.tsx
@@ -4,8 +4,8 @@ import { StyleSheet, View } from "react-native";
import { ExploreError } from "@/components/explore/explore-error";
import { ExploreFeed } from "@/components/explore/explore-feed";
-import { ExploreLoading } from "@/components/explore/explore-loading";
-import { SavedSpaceBackground } from "@/components/saved/saved-space-background";
+import { ExploreFeedSkeleton } from "@/components/explore/explore-feed-skeleton";
+import { EXPLORE_FEED_BODY_BG } from "@/constants/explore-feed-layout";
import { useCountryFeedStore } from "@/store/use-country-feed-store";
import { useSpatialContextStore } from "@/store/use-spatial-context-store";
@@ -71,7 +71,6 @@ export default function ExploreScreen() {
return (
-
{showError ? (
) : showInitialLoading || showHereLoading ? (
-
+
) : showFeed ? (
) : (
-
+
)}
);
@@ -98,6 +97,6 @@ export default function ExploreScreen() {
const styles = StyleSheet.create({
screen: {
flex: 1,
- backgroundColor: "#0b132b",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
},
});
diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx
deleted file mode 100644
index 96f2ad3..0000000
--- a/app/(tabs)/profile.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { router } from "expo-router";
-import { Pressable, ScrollView, Text, View } from "react-native";
-
-import { useOnboardingStore } from "@/store/use-onboarding-store";
-
-export default function ProfileScreen() {
- const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding);
-
- const handleOpenOnboarding = () => {
- resetOnboarding();
- router.replace("/onboarding");
- };
-
- return (
-
-
- Profile
- Coming in a later lesson
-
-
- {__DEV__ ? (
-
-
- View onboarding again
-
-
- ) : null}
-
- );
-}
diff --git a/app/(tabs)/profile/_layout.tsx b/app/(tabs)/profile/_layout.tsx
new file mode 100644
index 0000000..98e244b
--- /dev/null
+++ b/app/(tabs)/profile/_layout.tsx
@@ -0,0 +1,16 @@
+import { Stack } from "expo-router";
+
+export default function ProfileLayout() {
+ return (
+
+
+
+
+ );
+}
diff --git a/app/(tabs)/profile/index.tsx b/app/(tabs)/profile/index.tsx
new file mode 100644
index 0000000..15b2b02
--- /dev/null
+++ b/app/(tabs)/profile/index.tsx
@@ -0,0 +1,161 @@
+import { router } from "expo-router";
+import { StatusBar } from "expo-status-bar";
+import { useEffect } from "react";
+import { Dimensions, ScrollView, StyleSheet, View } from "react-native";
+import {
+ SafeAreaView,
+ useSafeAreaInsets,
+} from "react-native-safe-area-context";
+
+import { TAB_BAR_CONTENT_HEIGHT } from "@/components/bottom-tab-bar";
+import { ProfileHeroBackdrop } from "@/components/profile/profile-hero-backdrop";
+import { ProfileHeroHeader } from "@/components/profile/profile-hero-header";
+import { ProfileNavRow } from "@/components/profile/profile-nav-row";
+import {
+ WORLDLOOP_HEADER_TOP_PADDING,
+ WorldLoopHeader,
+} from "@/components/worldloop-header";
+import {
+ CULTURE_CHROME_TITLE_SIZE,
+ CULTURE_CHROME_TOUCH_SIZE,
+} from "@/constants/culture-chrome";
+import { PROFILE_HEADER_BOTTOM_GAP } from "@/constants/profile-theme";
+import { SPACE_SCREEN_BASE } from "@/constants/space-theme";
+import { useHeaderBackButton } from "@/hooks/use-header-back-button";
+import { useProfileStats } from "@/hooks/use-profile-stats";
+import { useCountryFeedStore } from "@/store/use-country-feed-store";
+import { useSavedCountriesStore } from "@/store/use-saved-countries-store";
+
+const HERO_HEIGHT_RATIO = 0.4;
+
+export default function ProfileScreen() {
+ const insets = useSafeAreaInsets();
+ const seedIfEmpty = useSavedCountriesStore((s) => s.seedIfEmpty);
+ const enrichFromFeed = useSavedCountriesStore((s) => s.enrichFromFeed);
+ const feedCountries = useCountryFeedStore((s) => s.countries);
+ const feedStatus = useCountryFeedStore((s) => s.status);
+ const loadInitialFeed = useCountryFeedStore((s) => s.loadInitialFeed);
+
+ const stats = useProfileStats();
+ const { visible: showBack, onBackPress } = useHeaderBackButton();
+ const heroHeight = Dimensions.get("window").height * HERO_HEIGHT_RATIO;
+
+ useEffect(() => {
+ const finishHydration = useSavedCountriesStore.persist.onFinishHydration(
+ () => {
+ seedIfEmpty();
+ },
+ );
+ if (useSavedCountriesStore.persist.hasHydrated()) {
+ seedIfEmpty();
+ }
+ return finishHydration;
+ }, [seedIfEmpty]);
+
+ useEffect(() => {
+ if (feedCountries.length === 0 && feedStatus === "idle") {
+ void loadInitialFeed();
+ }
+ }, [feedCountries.length, feedStatus, loadInitialFeed]);
+
+ useEffect(() => {
+ enrichFromFeed(feedCountries);
+ }, [feedCountries, enrichFromFeed]);
+
+ const scrollBottomPadding = TAB_BAR_CONTENT_HEIGHT + insets.bottom + 16;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ router.push("/(tabs)/saved")}
+ />
+ router.push("/(tabs)/map")}
+ />
+ router.push("/(tabs)/map")}
+ />
+ router.push("/(tabs)/profile/settings")}
+ />
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: SPACE_SCREEN_BASE,
+ },
+ scroll: {
+ flex: 1,
+ },
+ heroSection: {
+ position: "relative",
+ overflow: "hidden",
+ },
+ content: {
+ paddingHorizontal: 16,
+ paddingTop: 4,
+ gap: 12,
+ },
+ navRows: {
+ gap: 10,
+ },
+});
diff --git a/app/(tabs)/profile/settings.tsx b/app/(tabs)/profile/settings.tsx
new file mode 100644
index 0000000..138eb17
--- /dev/null
+++ b/app/(tabs)/profile/settings.tsx
@@ -0,0 +1,159 @@
+import { Ionicons } from "@expo/vector-icons";
+import { StatusBar } from "expo-status-bar";
+import { Alert, ScrollView, StyleSheet, Text, View } from "react-native";
+import {
+ SafeAreaView,
+ useSafeAreaInsets,
+} from "react-native-safe-area-context";
+
+import { TAB_BAR_CONTENT_HEIGHT } from "@/components/bottom-tab-bar";
+import { ProfileBackButton } from "@/components/profile/profile-back-button";
+import { ProfileLogOutButton } from "@/components/profile/profile-log-out-button";
+import { ProfileSettingsHeader } from "@/components/profile/profile-settings-header";
+import { ProfileSettingsRow } from "@/components/profile/profile-settings-row";
+import { ProfileStatsBar } from "@/components/profile/profile-stats-bar";
+import { PROFILE_ICON_MUTED } from "@/constants/profile-theme";
+import { SPACE_SCREEN_BASE } from "@/constants/space-theme";
+import { useHeaderBackButton } from "@/hooks/use-header-back-button";
+import { useProfileStats } from "@/hooks/use-profile-stats";
+import {
+ getLanguageLabel,
+ useProfileSettingsStore,
+} from "@/store/use-profile-settings-store";
+
+function showComingSoon(label: string) {
+ Alert.alert(label, "This feature is coming in a later lesson.");
+}
+
+export default function ProfileSettingsScreen() {
+ const insets = useSafeAreaInsets();
+ const darkModeEnabled = useProfileSettingsStore((s) => s.darkModeEnabled);
+ const languageCode = useProfileSettingsStore((s) => s.languageCode);
+ const setDarkModeEnabled = useProfileSettingsStore(
+ (s) => s.setDarkModeEnabled,
+ );
+ const stats = useProfileStats();
+ const { visible: showBack, onBackPress } = useHeaderBackButton();
+
+ const scrollBottomPadding = TAB_BAR_CONTENT_HEIGHT + insets.bottom + 16;
+
+ return (
+
+
+
+ {showBack ? (
+
+
+
+ ) : (
+
+ )}
+
+ WorldLoop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ showComingSoon("Personal information")}
+ />
+ showComingSoon("Notifications")}
+ />
+ showComingSoon("Offline maps")}
+ />
+ showComingSoon("Language")}
+ />
+
+ showComingSoon("Help center")}
+ />
+ showComingSoon("Privacy & terms")}
+ isLast
+ />
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: SPACE_SCREEN_BASE,
+ },
+ topChrome: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingHorizontal: 16,
+ paddingBottom: 8,
+ minHeight: 44,
+ },
+ sideSlot: {
+ width: 44,
+ height: 44,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ scroll: {
+ flex: 1,
+ },
+ sectionGap: {
+ marginTop: 16,
+ marginBottom: 16,
+ },
+ settingsCard: {
+ marginHorizontal: 16,
+ borderRadius: 16,
+ backgroundColor: "#1a2235",
+ overflow: "hidden",
+ },
+});
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 88a8045..95341e6 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,10 +1,13 @@
import "../global.css";
+import { ClerkProvider } from "@clerk/expo";
+import { tokenCache } from "@clerk/expo/token-cache";
import { Stack } from "expo-router";
import { useEffect } from "react";
import { View } from "react-native";
import { useAppFonts } from "@/hooks/use-app-fonts";
+import { clerkPublishableKey } from "@/lib/clerk";
import { useDiscoveryProgressStore } from "@/store/use-discovery-progress-store";
export default function RootLayout() {
@@ -26,20 +29,26 @@ export default function RootLayout() {
}
return (
-
-
-
-
-
-
-
- {__DEV__ ? : null}
-
-
+
+
+
+
+
+
+
+
+
+ {__DEV__ ? : null}
+
+
+
);
}
diff --git a/app/index.tsx b/app/index.tsx
index dcf6223..b7e7115 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,31 +1,16 @@
+import { useAuth } from "@clerk/expo";
import { Redirect } from "expo-router";
-import { useEffect, useState } from "react";
-
-import { useOnboardingStore } from "@/store/use-onboarding-store";
export default function Index() {
- const hasCompletedOnboarding = useOnboardingStore(
- (state) => state.hasCompletedOnboarding,
- );
- const [hydrated, setHydrated] = useState(
- useOnboardingStore.persist.hasHydrated(),
- );
-
- useEffect(() => {
- if (hydrated) return;
-
- return useOnboardingStore.persist.onFinishHydration(() => {
- setHydrated(true);
- });
- }, [hydrated]);
+ const { isSignedIn, isLoaded } = useAuth();
- if (!hydrated) {
+ if (!isLoaded) {
return null;
}
- if (!hasCompletedOnboarding) {
- return ;
+ if (isSignedIn) {
+ return ;
}
- return ;
+ return ;
}
diff --git a/app/oauth-callback.tsx b/app/oauth-callback.tsx
new file mode 100644
index 0000000..cf17505
--- /dev/null
+++ b/app/oauth-callback.tsx
@@ -0,0 +1,7 @@
+import * as WebBrowser from "expo-web-browser";
+
+WebBrowser.maybeCompleteAuthSession();
+
+export default function OAuthCallbackScreen() {
+ return null;
+}
diff --git a/components/auth/auth-apple-button.tsx b/components/auth/auth-apple-button.tsx
index 0f076a5..0d4b4c5 100644
--- a/components/auth/auth-apple-button.tsx
+++ b/components/auth/auth-apple-button.tsx
@@ -14,7 +14,7 @@ type AuthAppleButtonProps = {
export function AuthAppleButton({ onPress }: AuthAppleButtonProps) {
const handlePress = () => {
- void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onPress?.();
};
@@ -45,7 +45,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
},
buttonPressed: {
- opacity: 0.88,
+ backgroundColor: AUTH_COLORS.socialBgPressed,
+ borderColor: AUTH_COLORS.socialBorderPressed,
+ transform: [{ scale: 0.98 }],
},
label: {
color: AUTH_COLORS.textPrimary,
diff --git a/components/auth/auth-social-button.tsx b/components/auth/auth-social-button.tsx
index d00cdd4..796ef58 100644
--- a/components/auth/auth-social-button.tsx
+++ b/components/auth/auth-social-button.tsx
@@ -32,7 +32,7 @@ export function AuthSocialButton({ provider, onPress }: AuthSocialButtonProps) {
const config = PROVIDER_CONFIG[provider];
const handlePress = () => {
- void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onPress?.();
};
@@ -64,8 +64,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
},
buttonPressed: {
- opacity: 0.92,
- borderColor: "rgba(255, 255, 255, 0.22)",
+ backgroundColor: AUTH_COLORS.socialBgPressed,
+ borderColor: AUTH_COLORS.socialBorderPressed,
+ transform: [{ scale: 0.98 }],
},
label: {
color: AUTH_COLORS.socialText,
diff --git a/components/auth/auth-tabs-screen.tsx b/components/auth/auth-tabs-screen.tsx
index 9555446..3c6927c 100644
--- a/components/auth/auth-tabs-screen.tsx
+++ b/components/auth/auth-tabs-screen.tsx
@@ -1,7 +1,7 @@
import { useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useMemo, useState } from "react";
-import { StyleSheet, View } from "react-native";
+import { StyleSheet, Text, View } from "react-native";
import { AuthForgotPasswordLink } from "@/components/auth/auth-forgot-password-link";
import { AuthVerificationModal } from "@/components/auth/auth-verification-modal";
@@ -12,7 +12,7 @@ import { SignUpPrimaryButton } from "@/components/auth/sign-up/sign-up-primary-b
import { SignUpScreenShell } from "@/components/auth/sign-up/sign-up-screen-shell";
import { SignUpSocialButton } from "@/components/auth/sign-up/sign-up-social-button";
import { SIGN_UP_SPACING } from "@/constants/sign-up-theme";
-import { useAuthVerificationFlow } from "@/hooks/use-auth-verification-flow";
+import { useClerkAuthFlow } from "@/hooks/use-clerk-auth-flow";
import type { AuthTab } from "@/types/auth";
const LOGIN_HEADER = {
@@ -41,13 +41,20 @@ export function AuthTabsScreen({ initialTab }: AuthTabsScreenProps) {
const [registerConfirmPassword, setRegisterConfirmPassword] = useState("");
const {
+ isSubmitting,
+ formError,
+ verificationErrors,
+ verificationEmail,
isVerificationVisible,
- openVerification,
+ submitCredentials,
+ handleVerifyCode,
+ handleResendCode,
+ handleSSO,
closeVerification,
- completeVerification,
- } = useAuthVerificationFlow();
+ } = useClerkAuthFlow(isLogin);
- const verificationEmail = isLogin ? loginEmail.trim() : registerEmail.trim();
+ const activeEmail = isLogin ? loginEmail.trim() : registerEmail.trim();
+ const activePassword = isLogin ? loginPassword : registerPassword;
const canLogin = useMemo(
() => loginEmail.trim().length > 0 && loginPassword.length > 0,
@@ -63,6 +70,21 @@ export function AuthTabsScreen({ initialTab }: AuthTabsScreenProps) {
);
}, [registerEmail, registerPassword, registerConfirmPassword]);
+ const handlePrimaryPress = () => {
+ void submitCredentials(activeEmail, activePassword);
+ };
+
+ const handleGooglePress = () => {
+ void handleSSO("oauth_google");
+ };
+
+ const handleApplePress = () => {
+ void handleSSO("oauth_apple");
+ };
+
+ const verificationError =
+ verificationErrors?.code?.message ?? formError ?? null;
+
const loginForm = (
<>
@@ -95,18 +117,30 @@ export function AuthTabsScreen({ initialTab }: AuthTabsScreenProps) {
+ {formError && !isVerificationVisible ? (
+ {formError}
+ ) : null}
+
-
-
+
+
+ {formError && !isVerificationVisible ? (
+ {formError}
+ ) : null}
+
-
-
+
+
router.replace("/(auth)/sign-in")}
/>
+
+
>
);
@@ -189,9 +237,12 @@ export function AuthTabsScreen({ initialTab }: AuthTabsScreenProps) {
);
@@ -218,4 +269,11 @@ const styles = StyleSheet.create({
alignSelf: "stretch",
gap: SIGN_UP_SPACING.socialGap,
},
+ formError: {
+ color: "#F87171",
+ fontFamily: "Poppins-Regular",
+ fontSize: 13,
+ lineHeight: 18,
+ textAlign: "center",
+ },
});
diff --git a/components/auth/auth-verification-modal.tsx b/components/auth/auth-verification-modal.tsx
index 9818343..8e4faa3 100644
--- a/components/auth/auth-verification-modal.tsx
+++ b/components/auth/auth-verification-modal.tsx
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import {
+ ActivityIndicator,
KeyboardAvoidingView,
Modal,
Platform,
@@ -22,15 +23,21 @@ const CODE_LENGTH = 6;
type AuthVerificationModalProps = {
visible: boolean;
email: string;
+ error?: string | null;
+ loading?: boolean;
onClose: () => void;
- onComplete: () => void;
+ onVerify: (code: string) => void | Promise;
+ onResend?: () => void | Promise;
};
export function AuthVerificationModal({
visible,
email,
+ error = null,
+ loading = false,
onClose,
- onComplete,
+ onVerify,
+ onResend,
}: AuthVerificationModalProps) {
const insets = useSafeAreaInsets();
const inputRef = useRef(null);
@@ -53,9 +60,9 @@ export function AuthVerificationModal({
const digits = value.replace(/\D/g, "").slice(0, CODE_LENGTH);
setCode(digits);
- if (digits.length === CODE_LENGTH) {
+ if (digits.length === CODE_LENGTH && !loading) {
inputRef.current?.blur();
- onComplete();
+ void onVerify(digits);
}
};
@@ -111,6 +118,30 @@ export function AuthVerificationModal({
})}
+ {error ? {error} : null}
+
+ {loading ? (
+
+ ) : null}
+
+ {onResend ? (
+ void onResend()}
+ disabled={loading}
+ style={({ pressed }) => [
+ styles.resendButton,
+ pressed && styles.resendButtonPressed,
+ ]}
+ >
+ Resend code
+
+ ) : null}
+
@@ -205,6 +237,29 @@ const styles = StyleSheet.create({
fontSize: 20,
lineHeight: 24,
},
+ error: {
+ color: "#F87171",
+ fontFamily: "Poppins-Regular",
+ fontSize: 13,
+ lineHeight: 18,
+ textAlign: "center",
+ },
+ loader: {
+ marginTop: -4,
+ },
+ resendButton: {
+ alignSelf: "center",
+ paddingVertical: 4,
+ },
+ resendButtonPressed: {
+ opacity: 0.75,
+ },
+ resendLabel: {
+ color: AUTH_COLORS.gold,
+ fontFamily: "Poppins-Medium",
+ fontSize: 14,
+ lineHeight: 20,
+ },
hiddenInput: {
position: "absolute",
opacity: 0,
diff --git a/components/auth/sign-up/sign-up-primary-button.tsx b/components/auth/sign-up/sign-up-primary-button.tsx
index b401df7..c38fc9e 100644
--- a/components/auth/sign-up/sign-up-primary-button.tsx
+++ b/components/auth/sign-up/sign-up-primary-button.tsx
@@ -53,7 +53,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
},
buttonDisabled: {
- opacity: 0.45,
+ opacity: 0.35,
},
buttonPressed: {
opacity: 0.92,
diff --git a/components/auth/sign-up/sign-up-social-button.tsx b/components/auth/sign-up/sign-up-social-button.tsx
index ad64365..d2f4ff9 100644
--- a/components/auth/sign-up/sign-up-social-button.tsx
+++ b/components/auth/sign-up/sign-up-social-button.tsx
@@ -14,6 +14,7 @@ type SignUpSocialProvider = "google" | "apple";
type SignUpSocialButtonProps = {
provider: SignUpSocialProvider;
onPress?: () => void;
+ disabled?: boolean;
};
const SOCIAL_ICON_SIZE = 18;
@@ -32,11 +33,13 @@ const PROVIDER_CONFIG = {
export function SignUpSocialButton({
provider,
onPress,
+ disabled = false,
}: SignUpSocialButtonProps) {
const config = PROVIDER_CONFIG[provider];
const handlePress = () => {
- void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (disabled) return;
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onPress?.();
};
@@ -44,8 +47,14 @@ export function SignUpSocialButton({
[styles.button, pressed && styles.buttonPressed]}
+ style={({ pressed }) => [
+ styles.button,
+ disabled && styles.buttonDisabled,
+ pressed && !disabled && styles.buttonPressed,
+ ]}
>
{provider === "google" ? (
@@ -76,8 +85,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
},
buttonPressed: {
- opacity: 0.92,
- borderColor: "rgba(255, 255, 255, 0.22)",
+ backgroundColor: SIGN_UP_COLORS.socialBgPressed,
+ borderColor: SIGN_UP_COLORS.socialBorderPressed,
+ transform: [{ scale: 0.98 }],
+ },
+ buttonDisabled: {
+ opacity: 0.45,
},
content: {
flexDirection: "row",
diff --git a/components/culture/culture-feed-skeleton.tsx b/components/culture/culture-feed-skeleton.tsx
new file mode 100644
index 0000000..f89a461
--- /dev/null
+++ b/components/culture/culture-feed-skeleton.tsx
@@ -0,0 +1,231 @@
+import { LinearGradient } from "expo-linear-gradient";
+import { useEffect, useRef } from "react";
+import { Animated, StyleSheet, View, type DimensionValue } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { TAB_BAR_CONTENT_HEIGHT } from "@/components/bottom-tab-bar";
+import { CultureTopBar } from "@/components/culture/culture-top-bar";
+import {
+ CULTURE_CHROME_ICON_SIZE,
+ CULTURE_CHROME_RAIL_GAP,
+} from "@/constants/culture-chrome";
+
+const CULTURE_FEED_BODY_BG = "#0b132b";
+const SKELETON_COLOR = "rgba(255, 255, 255, 0.14)";
+const SKELETON_COLOR_SOFT = "rgba(255, 255, 255, 0.08)";
+
+/** Matches `CultureOverlay` — card sits into tab bar chrome. */
+const CULTURE_OVERLAY_TAB_CLEARANCE = -70;
+/** Matches `CultureOverlay` action rail offset. */
+const CULTURE_ACTION_RAIL_CLEARANCE = -16;
+const BOTTOM_SCRIM_HEIGHT = "38%";
+
+const FLAG_SIZE = 28;
+const RAIL_ACTION_COUNT = 5;
+
+type SkeletonBoneProps = {
+ pulse: Animated.Value;
+ width: DimensionValue;
+ height: DimensionValue;
+ borderRadius?: number;
+ color?: string;
+ style?: object;
+};
+
+function SkeletonBone({
+ pulse,
+ width,
+ height,
+ borderRadius = 8,
+ color = SKELETON_COLOR,
+ style,
+}: SkeletonBoneProps) {
+ return (
+
+ );
+}
+
+function SkeletonRailAction({ pulse }: { pulse: Animated.Value }) {
+ return (
+
+
+
+
+ );
+}
+
+function useSkeletonPulse() {
+ const pulse = useRef(new Animated.Value(0.45)).current;
+
+ useEffect(() => {
+ const animation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(pulse, {
+ toValue: 0.9,
+ duration: 900,
+ useNativeDriver: true,
+ }),
+ Animated.timing(pulse, {
+ toValue: 0.45,
+ duration: 900,
+ useNativeDriver: true,
+ }),
+ ]),
+ );
+
+ animation.start();
+ return () => animation.stop();
+ }, [pulse]);
+
+ return pulse;
+}
+
+export function CultureFeedSkeleton() {
+ const insets = useSafeAreaInsets();
+ const pulse = useSkeletonPulse();
+
+ const overlayBottomOffset =
+ TAB_BAR_CONTENT_HEIGHT + insets.bottom + CULTURE_OVERLAY_TAB_CLEARANCE;
+ const railBottomOffset =
+ insets.bottom + TAB_BAR_CONTENT_HEIGHT + CULTURE_ACTION_RAIL_CLEARANCE;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: RAIL_ACTION_COUNT }, (_, index) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ feed: {
+ flex: 1,
+ },
+ feedBody: {
+ flex: 1,
+ backgroundColor: CULTURE_FEED_BODY_BG,
+ overflow: "hidden",
+ },
+ mediaSkeleton: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ scrim: {
+ position: "absolute",
+ left: 0,
+ right: 0,
+ bottom: 0,
+ height: BOTTOM_SCRIM_HEIGHT,
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: "flex-end",
+ paddingLeft: 16,
+ paddingRight: 64,
+ zIndex: 2,
+ },
+ infoBlock: {
+ gap: 10,
+ },
+ titleRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ titleBone: {
+ flexShrink: 1,
+ },
+ actionRail: {
+ position: "absolute",
+ right: 12,
+ alignItems: "center",
+ gap: CULTURE_CHROME_RAIL_GAP,
+ zIndex: 3,
+ },
+ railAction: {
+ alignItems: "center",
+ gap: 4,
+ },
+ bone: {
+ backgroundColor: SKELETON_COLOR,
+ },
+});
diff --git a/components/culture/culture-feed.tsx b/components/culture/culture-feed.tsx
index 9465c95..4d4f60c 100644
--- a/components/culture/culture-feed.tsx
+++ b/components/culture/culture-feed.tsx
@@ -1,5 +1,5 @@
import { useIsFocused } from "@react-navigation/native";
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
FlatList,
@@ -7,6 +7,8 @@ import {
StyleSheet,
View,
type LayoutChangeEvent,
+ type NativeScrollEvent,
+ type NativeSyntheticEvent,
type ViewToken,
} from "react-native";
@@ -37,13 +39,31 @@ export function CultureFeed() {
);
const refreshCultureFeed = useCultureFeedStore((s) => s.refreshCultureFeed);
const [isRefreshing, setIsRefreshing] = useState(false);
+ const [isHeaderVisible, setIsHeaderVisible] = useState(true);
+ const lastScrollOffsetYRef = useRef(0);
+ const lastVisibleIndexRef = useRef(0);
- const onViewableItemsChanged = useRef(
+ useEffect(() => {
+ setIsHeaderVisible(true);
+ lastScrollOffsetYRef.current = 0;
+ lastVisibleIndexRef.current = 0;
+ }, [feedListKey]);
+
+ const onViewableItemsChanged = useCallback(
({ viewableItems }: { viewableItems: ViewToken[] }) => {
const first = viewableItems[0];
if (first?.index == null) return;
const index = first.index;
+ const previousIndex = lastVisibleIndexRef.current;
+
+ if (index > previousIndex) {
+ setIsHeaderVisible(false);
+ } else if (index < previousIndex) {
+ setIsHeaderVisible(true);
+ }
+
+ lastVisibleIndexRef.current = index;
setCurrentIndex(index);
const state = useCultureFeedStore.getState();
@@ -61,7 +81,8 @@ export function CultureFeed() {
void extendCultureFeedAtEnd();
}
},
- ).current;
+ [extendCultureFeedAtEnd, setCurrentIndex],
+ );
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
@@ -110,6 +131,22 @@ export function CultureFeed() {
}
}, [refreshCultureFeed]);
+ const onFeedScroll = useCallback(
+ (event: NativeSyntheticEvent) => {
+ const offsetY = event.nativeEvent.contentOffset.y;
+ const delta = offsetY - lastScrollOffsetYRef.current;
+
+ if (delta < -8) {
+ setIsHeaderVisible(true);
+ } else if (delta > 8) {
+ setIsHeaderVisible(false);
+ }
+
+ lastScrollOffsetYRef.current = offsetY;
+ },
+ [],
+ );
+
return (
@@ -125,6 +162,8 @@ export function CultureFeed() {
pagingEnabled
showsVerticalScrollIndicator={false}
decelerationRate="fast"
+ scrollEventThrottle={16}
+ onScroll={onFeedScroll}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
getItemLayout={(_, index) => ({
@@ -162,7 +201,7 @@ export function CultureFeed() {
) : null}
-
+
);
}
diff --git a/components/culture/culture-top-bar.tsx b/components/culture/culture-top-bar.tsx
index 1edad74..9efacff 100644
--- a/components/culture/culture-top-bar.tsx
+++ b/components/culture/culture-top-bar.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -11,11 +11,16 @@ import {
CULTURE_CHROME_ICON_SIZE,
CULTURE_CHROME_TITLE_SIZE,
CULTURE_CHROME_TOUCH_SIZE,
+ CULTURE_HEADER_HORIZONTAL_PADDING,
} from "@/constants/culture-chrome";
import { useCultureFeedStore } from "@/store/use-culture-feed-store";
import { useSearchUiStore } from "@/store/use-search-ui-store";
-export function CultureTopBar() {
+type CultureTopBarProps = {
+ visible?: boolean;
+};
+
+export function CultureTopBar({ visible = true }: CultureTopBarProps) {
const insets = useSafeAreaInsets();
const [isFeedMenuOpen, setIsFeedMenuOpen] = useState(false);
const openSearch = useSearchUiStore((s) => s.openSearch);
@@ -24,6 +29,16 @@ export function CultureTopBar() {
);
const isSortSheetOpen = useCultureFeedStore((s) => s.isSortSheetOpen);
+ useEffect(() => {
+ if (!visible && isFeedMenuOpen) {
+ setIsFeedMenuOpen(false);
+ }
+ }, [isFeedMenuOpen, visible]);
+
+ if (!visible) {
+ return null;
+ }
+
return (
diff --git a/components/explore/country-feed-page.tsx b/components/explore/country-feed-page.tsx
index 5ea0156..edc82d6 100644
--- a/components/explore/country-feed-page.tsx
+++ b/components/explore/country-feed-page.tsx
@@ -3,15 +3,17 @@ import {
useEffect,
useLayoutEffect,
useMemo,
+ useRef,
useState,
} from "react";
-import { Platform, StyleSheet, View } from "react-native";
+import { Animated, Platform, StyleSheet, View } from "react-native";
import { prefetchCountryImage } from "@/components/explore/country-image";
import { ExploreCountryCard } from "@/components/explore/explore-country-card";
import { HeroImagePager } from "@/components/explore/hero-image-pager";
import { MediaCarousel } from "@/components/explore/media-carousel";
import {
+ EXPLORE_FEED_BODY_BG,
EXPLORE_FEED_BOTTOM_INSET,
EXPLORE_FEED_SURFACE_RADIUS,
} from "@/constants/explore-feed-layout";
@@ -24,11 +26,12 @@ import { prefetchCountryProfile } from "@/lib/prefetch-country-profiles";
import type { Country } from "@/types/country";
const DOTS_BOTTOM_INSET = 5;
+const HERO_INSET_ANIMATION_MS = 220;
type CountryFeedPageProps = {
country: Country;
pageHeight: number;
- headerContentInset: number;
+ heroTopInset: number;
isActive?: boolean;
onActiveHeroIndexChange?: (index: number) => void;
};
@@ -36,13 +39,22 @@ type CountryFeedPageProps = {
export function CountryFeedPage({
country,
pageHeight,
- headerContentInset,
+ heroTopInset,
isActive = false,
onActiveHeroIndexChange,
}: CountryFeedPageProps) {
const images = useMemo(() => getCountryImages(country), [country.name]);
const [heroIndex, setHeroIndex] = useState(0);
const [heroLayout, setHeroLayout] = useState({ width: 0, height: 0 });
+ const paddingTopAnim = useRef(new Animated.Value(heroTopInset)).current;
+
+ useEffect(() => {
+ Animated.timing(paddingTopAnim, {
+ toValue: heroTopInset,
+ duration: HERO_INSET_ANIMATION_MS,
+ useNativeDriver: false,
+ }).start();
+ }, [heroTopInset, paddingTopAnim]);
const heroSlides = useMemo(
() => Array.from({ length: images.length }, (_, index) => `${index}`),
@@ -81,8 +93,16 @@ export function CountryFeedPage({
}, [country]);
return (
-
-
+
+
@@ -143,8 +163,8 @@ export function CountryFeedPage({
-
-
+
+
);
}
@@ -185,7 +205,7 @@ const styles = StyleSheet.create({
flex: 1,
width: "100%",
overflow: "hidden",
- backgroundColor: "#0b132b",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
},
cardRegion: {
flexShrink: 0,
diff --git a/components/explore/country-image.tsx b/components/explore/country-image.tsx
index a89b2f8..df3ac96 100644
--- a/components/explore/country-image.tsx
+++ b/components/explore/country-image.tsx
@@ -8,6 +8,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { StyleSheet, View, type StyleProp } from "react-native";
import { FlagBadge } from "@/components/explore/flag-badge";
+import { EXPLORE_FEED_BODY_BG } from "@/constants/explore-feed-layout";
import { normalizeImageUrl } from "@/lib/normalize-image-url";
const WIKIMEDIA_HEADERS = {
@@ -151,7 +152,7 @@ export function CountryImage({
const styles = StyleSheet.create({
fallback: {
- backgroundColor: "#0b132b",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
alignItems: "center",
justifyContent: "center",
},
diff --git a/components/explore/explore-action-rail.tsx b/components/explore/explore-action-rail.tsx
index 7f67dfe..2da0a6c 100644
--- a/components/explore/explore-action-rail.tsx
+++ b/components/explore/explore-action-rail.tsx
@@ -33,6 +33,8 @@ import {
EXPLORE_FEED_RAIL_HORIZONTAL_PADDING,
EXPLORE_FEED_SURFACE_RADIUS,
} from "@/constants/explore-feed-layout";
+import { openCountryInCulture } from "@/features/navigation/open-country-in-culture";
+import { hasCultureVideo } from "@/lib/format-country";
import { focusCountryOnMap } from "@/lib/open-country-on-map";
import {
DEFAULT_FEED_SORT_FIELD,
@@ -60,6 +62,7 @@ const SHEET_ENTER = SlideInDown.springify()
/** Header rail: compact touch target + vertical padding (4 + 4). */
const RAIL_HEADER_VERTICAL_PADDING = 8;
+const RAIL_HEADER_ICON_GAP = 6;
const RAIL_HEADER_BORDER_RADIUS = EXPLORE_FEED_SURFACE_RADIUS;
const RAIL_SURFACE_BLUR_INTENSITY = 32;
const RAIL_SURFACE_TINT = "rgba(11, 19, 43, 0.38)";
@@ -422,6 +425,12 @@ export function ExploreActionRail({
toggleSaved(country);
};
+ const handleOpenCultureVideo = () => {
+ openCountryInCulture(country);
+ };
+
+ const hasVideo = hasCultureVideo(country);
+
return (
<>
+
+
+
+
-
-
-
-
) : variant === "culture" ? (
@@ -607,7 +624,7 @@ export function ExploreActionRail({
)}
- {variant !== "culture" ? (
+ {variant === "overlay" ? (
) : null}
-
-
-
-
-
-
- Sort feed
- {currentSortSummary ? (
- {currentSortSummary}
- ) : null}
+ {variant !== "header" ? (
+
+
+
+
+
+
+ Sort feed
+ {currentSortSummary ? (
+
+ {currentSortSummary}
+
+ ) : null}
+
+ [
+ styles.closeButton,
+ pressed && styles.optionPressed,
+ ]}
+ >
+
+
- [
- styles.closeButton,
- pressed && styles.optionPressed,
- ]}
- >
-
-
-
-
- Shuffle
- setDraftRandom((prev) => !prev)}
- />
+
+ Shuffle
+ setDraftRandom((prev) => !prev)}
+ />
-
+
- Sort by
-
- setDraftField("name")}
- />
- setDraftField("population")}
- />
-
+ Sort by
+
+ setDraftField("name")}
+ />
+ setDraftField("population")}
+ />
+
-
+
- Order
-
- setDraftOrder("asc")}
- />
- setDraftOrder("desc")}
- />
+ Order
+
+ setDraftOrder("asc")}
+ />
+ setDraftOrder("desc")}
+ />
+
-
-
- [
- styles.actionButton,
- styles.cancelButton,
- pressed && styles.optionPressed,
- ]}
- >
- Cancel
-
- [
- styles.actionButton,
- styles.applyButton,
- !hasSortChanges && styles.applyButtonDisabled,
- pressed && hasSortChanges && styles.optionPressed,
- ]}
- >
-
+ [
+ styles.actionButton,
+ styles.cancelButton,
+ pressed && styles.optionPressed,
]}
>
- Apply
-
-
-
-
-
-
+ Cancel
+
+ [
+ styles.actionButton,
+ styles.applyButton,
+ !hasSortChanges && styles.applyButtonDisabled,
+ pressed && hasSortChanges && styles.optionPressed,
+ ]}
+ >
+
+ Apply
+
+
+
+
+
+
+ ) : null}
>
);
}
@@ -840,7 +861,7 @@ const styles = StyleSheet.create({
railHeader: {
flexDirection: "row",
alignItems: "center",
- gap: 2,
+ gap: RAIL_HEADER_ICON_GAP,
paddingVertical: RAIL_HEADER_VERTICAL_PADDING / 2,
paddingHorizontal: EXPLORE_FEED_RAIL_HORIZONTAL_PADDING,
borderRadius: RAIL_HEADER_BORDER_RADIUS,
diff --git a/components/explore/explore-country-more-menus.tsx b/components/explore/explore-country-more-menus.tsx
new file mode 100644
index 0000000..aed84fd
--- /dev/null
+++ b/components/explore/explore-country-more-menus.tsx
@@ -0,0 +1,713 @@
+import { Ionicons } from "@expo/vector-icons";
+import { useState } from "react";
+import {
+ Alert,
+ Modal,
+ Pressable,
+ Share,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Animated, { SlideInDown } from "react-native-reanimated";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import {
+ DEFAULT_FEED_SORT_FIELD,
+ DEFAULT_FEED_SORT_ORDER,
+ useCountryFeedStore,
+ type FeedSortField,
+ type FeedSortOrder,
+} from "@/store/use-country-feed-store";
+import type { Country } from "@/types/country";
+
+const SHEET_ENTER = SlideInDown.springify()
+ .damping(20)
+ .stiffness(150)
+ .mass(0.85);
+
+type ExploreCountryMoreMenusProps = {
+ country: Country | undefined;
+ isMoreMenuOpen: boolean;
+ onCloseMoreMenu: () => void;
+};
+
+function getOrderLabels(field: FeedSortField): { asc: string; desc: string } {
+ if (field === "population") {
+ return { asc: "Low → High", desc: "High → Low" };
+ }
+ return { asc: "A → Z", desc: "Z → A" };
+}
+
+function getSortSummary(
+ field: FeedSortField,
+ order: FeedSortOrder,
+): string | null {
+ if (order === "random") return null;
+ const orderLabel = getOrderLabels(field)[order];
+ const fieldLabel = field === "population" ? "Population" : "Name";
+ return `${fieldLabel} · ${orderLabel}`;
+}
+
+export function getExploreHasCustomSort(
+ sortField: FeedSortField | null | undefined,
+ sortOrder: FeedSortOrder | null | undefined,
+): boolean {
+ const field = sortField ?? DEFAULT_FEED_SORT_FIELD;
+ const order = sortOrder ?? DEFAULT_FEED_SORT_ORDER;
+ return field !== DEFAULT_FEED_SORT_FIELD || order !== DEFAULT_FEED_SORT_ORDER;
+}
+
+type SortRadioOptionProps = {
+ label: string;
+ selected: boolean;
+ disabled?: boolean;
+ onPress: () => void;
+};
+
+function SortRadioOption({
+ label,
+ selected,
+ disabled = false,
+ onPress,
+}: SortRadioOptionProps) {
+ return (
+ [
+ styles.radioRow,
+ disabled && styles.radioRowDisabled,
+ pressed && !disabled && styles.optionPressed,
+ ]}
+ >
+
+ {selected && !disabled ? : null}
+
+
+ {label}
+
+
+ );
+}
+
+type SortCheckboxOptionProps = {
+ label: string;
+ checked: boolean;
+ onPress: () => void;
+};
+
+function SortCheckboxOption({
+ label,
+ checked,
+ onPress,
+}: SortCheckboxOptionProps) {
+ return (
+ [
+ styles.radioRow,
+ pressed && styles.optionPressed,
+ ]}
+ >
+
+ {checked ? ✓ : null}
+
+
+ {label}
+
+
+ );
+}
+
+type MoreMenuRowProps = {
+ icon: keyof typeof Ionicons.glyphMap;
+ label: string;
+ subtitle?: string;
+ disabled?: boolean;
+ active?: boolean;
+ onPress: () => void;
+};
+
+function MoreMenuRow({
+ icon,
+ label,
+ subtitle,
+ disabled = false,
+ active = false,
+ onPress,
+}: MoreMenuRowProps) {
+ return (
+ [
+ styles.menuRow,
+ disabled && styles.menuRowDisabled,
+ pressed && !disabled && styles.optionPressed,
+ ]}
+ >
+
+
+
+
+
+ {label}
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+ {!disabled ? (
+
+ ) : null}
+
+ );
+}
+
+export function ExploreCountryMoreMenus({
+ country,
+ isMoreMenuOpen,
+ onCloseMoreMenu,
+}: ExploreCountryMoreMenusProps) {
+ const insets = useSafeAreaInsets();
+ const sortField = useCountryFeedStore((s) => s.sortField);
+ const sortOrder = useCountryFeedStore((s) => s.sortOrder);
+ const setSort = useCountryFeedStore((s) => s.setSort);
+ const [isSortModalOpen, setIsSortModalOpen] = useState(false);
+ const [draftRandom, setDraftRandom] = useState(false);
+ const [draftField, setDraftField] = useState(
+ sortField ?? DEFAULT_FEED_SORT_FIELD,
+ );
+ const [draftOrder, setDraftOrder] = useState(
+ sortOrder ?? DEFAULT_FEED_SORT_ORDER,
+ );
+
+ const currentField = sortField ?? DEFAULT_FEED_SORT_FIELD;
+ const currentOrder = sortOrder ?? DEFAULT_FEED_SORT_ORDER;
+ const sortOptionsDisabled = draftRandom;
+ const hasSortChanges =
+ (currentOrder === "random") !== draftRandom ||
+ (!draftRandom &&
+ (draftField !== currentField || draftOrder !== currentOrder));
+ const hasCustomSort = getExploreHasCustomSort(sortField, sortOrder);
+ const orderLabels = getOrderLabels(draftField);
+ const currentSortSummary = getSortSummary(currentField, currentOrder);
+ const countryName = country?.name ?? "this country";
+
+ const handleShare = async () => {
+ if (!country) return;
+ try {
+ await Share.share({
+ message: `Discover ${country.name} on WorldLoop!`,
+ });
+ } catch {
+ Alert.alert("Share", "Unable to share right now.");
+ }
+ };
+
+ const handleOpenSortModal = () => {
+ const random = currentOrder === "random";
+ setDraftField(currentField);
+ setDraftRandom(random);
+ setDraftOrder(random ? "asc" : currentOrder);
+ onCloseMoreMenu();
+ setIsSortModalOpen(true);
+ };
+
+ const handleCloseSortModal = () => {
+ setIsSortModalOpen(false);
+ };
+
+ const handleApplySort = () => {
+ setSort(draftField, draftRandom ? "random" : draftOrder);
+ setIsSortModalOpen(false);
+ };
+
+ const handleShareFromMenu = () => {
+ onCloseMoreMenu();
+ void handleShare();
+ };
+
+ return (
+ <>
+
+
+
+
+
+ More actions
+ [
+ styles.closeButton,
+ pressed && styles.optionPressed,
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+ {}}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Sort feed
+ {currentSortSummary ? (
+ {currentSortSummary}
+ ) : null}
+
+ [
+ styles.closeButton,
+ pressed && styles.optionPressed,
+ ]}
+ >
+
+
+
+
+
+ Shuffle
+ setDraftRandom((prev) => !prev)}
+ />
+
+
+
+ Sort by
+
+ setDraftField("name")}
+ />
+ setDraftField("population")}
+ />
+
+
+
+
+ Order
+
+ setDraftOrder("asc")}
+ />
+ setDraftOrder("desc")}
+ />
+
+
+
+
+ [
+ styles.actionButton,
+ styles.cancelButton,
+ pressed && styles.optionPressed,
+ ]}
+ >
+ Cancel
+
+ [
+ styles.actionButton,
+ styles.applyButton,
+ !hasSortChanges && styles.applyButtonDisabled,
+ pressed && hasSortChanges && styles.optionPressed,
+ ]}
+ >
+
+ Apply
+
+
+
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ optionPressed: {
+ opacity: 0.78,
+ },
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.55)",
+ justifyContent: "flex-end",
+ },
+ sheet: {
+ width: "100%",
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ paddingTop: 12,
+ paddingHorizontal: 16,
+ backgroundColor: "#111827",
+ borderWidth: 1,
+ borderBottomWidth: 0,
+ borderColor: "rgba(255, 255, 255, 0.14)",
+ gap: 4,
+ },
+ sheetHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: 8,
+ marginBottom: 2,
+ },
+ sheetTitleGroup: {
+ flex: 1,
+ gap: 1,
+ },
+ sheetTitle: {
+ fontSize: 16,
+ lineHeight: 20,
+ fontFamily: "Poppins-SemiBold",
+ color: "#ffffff",
+ },
+ sheetSubtitle: {
+ fontSize: 12,
+ lineHeight: 16,
+ fontFamily: "Poppins-Regular",
+ color: "rgba(255, 255, 255, 0.55)",
+ },
+ closeButton: {
+ width: 28,
+ height: 28,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ menuPanel: {
+ gap: 0,
+ marginTop: 4,
+ },
+ menuRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ minHeight: 52,
+ paddingVertical: 6,
+ },
+ menuRowDisabled: {
+ opacity: 0.55,
+ },
+ menuIconWrap: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
+ },
+ menuIconWrapActive: {
+ backgroundColor: "rgba(251, 191, 36, 0.15)",
+ },
+ menuTextGroup: {
+ flex: 1,
+ gap: 1,
+ },
+ menuLabel: {
+ fontSize: 14,
+ lineHeight: 18,
+ fontFamily: "Poppins-SemiBold",
+ color: "#ffffff",
+ },
+ menuLabelActive: {
+ color: "#fbbf24",
+ },
+ menuLabelDisabled: {
+ color: "rgba(255, 255, 255, 0.55)",
+ },
+ menuSubtitle: {
+ fontSize: 12,
+ lineHeight: 16,
+ fontFamily: "Poppins-Regular",
+ color: "rgba(255, 255, 255, 0.5)",
+ },
+ menuDivider: {
+ height: 1,
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
+ },
+ tabPanel: {
+ gap: 0,
+ },
+ sectionLabel: {
+ marginTop: 2,
+ marginBottom: 2,
+ fontSize: 12,
+ lineHeight: 16,
+ fontFamily: "Poppins-Medium",
+ color: "rgba(255, 255, 255, 0.65)",
+ textAlign: "left",
+ alignSelf: "flex-start",
+ },
+ sectionDivider: {
+ height: 1,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ marginVertical: 6,
+ },
+ radioGroup: {
+ gap: 0,
+ },
+ radioRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ minHeight: 40,
+ paddingVertical: 0,
+ },
+ radioRowDisabled: {
+ opacity: 0.4,
+ },
+ radioOuter: {
+ width: 20,
+ height: 20,
+ borderRadius: 10,
+ borderWidth: 2,
+ borderColor: "rgba(255, 255, 255, 0.45)",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ radioOuterSelected: {
+ borderColor: "#fbbf24",
+ },
+ radioOuterDisabled: {
+ borderColor: "rgba(255, 255, 255, 0.2)",
+ },
+ radioInner: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ backgroundColor: "#fbbf24",
+ },
+ checkboxOuter: {
+ width: 20,
+ height: 20,
+ borderRadius: 4,
+ borderWidth: 2,
+ borderColor: "rgba(255, 255, 255, 0.45)",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ checkboxOuterChecked: {
+ borderColor: "#fbbf24",
+ backgroundColor: "#fbbf24",
+ },
+ checkboxMark: {
+ fontSize: 12,
+ lineHeight: 14,
+ fontFamily: "Poppins-Bold",
+ color: "#0b132b",
+ marginTop: -1,
+ },
+ radioLabel: {
+ fontSize: 13,
+ lineHeight: 18,
+ fontFamily: "Poppins-Medium",
+ color: "rgba(255, 255, 255, 0.72)",
+ },
+ radioLabelSelected: {
+ color: "#ffffff",
+ fontFamily: "Poppins-SemiBold",
+ },
+ radioLabelDisabled: {
+ color: "rgba(255, 255, 255, 0.35)",
+ },
+ modalActions: {
+ marginTop: 4,
+ flexDirection: "row",
+ gap: 8,
+ alignItems: "center",
+ },
+ actionButton: {
+ flex: 1,
+ minHeight: 44,
+ borderRadius: 10,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ cancelButton: {
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.24)",
+ },
+ applyButton: {
+ backgroundColor: "#fbbf24",
+ },
+ applyButtonDisabled: {
+ backgroundColor: "rgba(148, 163, 184, 0.35)",
+ },
+ cancelText: {
+ fontSize: 13,
+ fontFamily: "Poppins-SemiBold",
+ color: "#ffffff",
+ },
+ applyText: {
+ fontSize: 13,
+ fontFamily: "Poppins-SemiBold",
+ color: "#0b132b",
+ },
+ applyTextDisabled: {
+ color: "rgba(255, 255, 255, 0.65)",
+ },
+});
diff --git a/components/explore/explore-feed-skeleton.tsx b/components/explore/explore-feed-skeleton.tsx
new file mode 100644
index 0000000..1ce71dc
--- /dev/null
+++ b/components/explore/explore-feed-skeleton.tsx
@@ -0,0 +1,199 @@
+import { useEffect, useRef } from "react";
+import { Animated, StyleSheet, View, type DimensionValue } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { ExploreTopBar } from "@/components/explore/explore-top-bar";
+import {
+ EXPLORE_FEED_BODY_BG,
+ EXPLORE_FEED_BOTTOM_INSET,
+ EXPLORE_FEED_SURFACE_RADIUS,
+ getExploreHeroTopInset,
+} from "@/constants/explore-feed-layout";
+
+const SKELETON_COLOR = "rgba(255, 255, 255, 0.14)";
+
+type SkeletonBoneProps = {
+ pulse: Animated.Value;
+ width: DimensionValue;
+ height: DimensionValue;
+ borderRadius?: number;
+ style?: object;
+};
+
+function SkeletonBone({
+ pulse,
+ width,
+ height,
+ borderRadius = 8,
+ style,
+}: SkeletonBoneProps) {
+ return (
+
+ );
+}
+
+function useSkeletonPulse() {
+ const pulse = useRef(new Animated.Value(0.4)).current;
+
+ useEffect(() => {
+ const animation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(pulse, {
+ toValue: 0.85,
+ duration: 900,
+ useNativeDriver: true,
+ }),
+ Animated.timing(pulse, {
+ toValue: 0.4,
+ duration: 900,
+ useNativeDriver: true,
+ }),
+ ]),
+ );
+
+ animation.start();
+ return () => animation.stop();
+ }, [pulse]);
+
+ return pulse;
+}
+
+export function ExploreFeedSkeleton() {
+ const insets = useSafeAreaInsets();
+ const pulse = useSkeletonPulse();
+ const heroTopInset = getExploreHeroTopInset(insets.top);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ feed: {
+ flex: 1,
+ },
+ feedBody: {
+ flex: 1,
+ backgroundColor: EXPLORE_FEED_BODY_BG,
+ },
+ pageContent: {
+ flex: 1,
+ },
+ feedUnit: {
+ flex: 1,
+ width: "100%",
+ overflow: "hidden",
+ borderBottomLeftRadius: EXPLORE_FEED_SURFACE_RADIUS,
+ borderBottomRightRadius: EXPLORE_FEED_SURFACE_RADIUS,
+ },
+ heroRegion: {
+ flex: 1,
+ width: "100%",
+ minHeight: 0,
+ },
+ heroSkeleton: {
+ flex: 1,
+ },
+ cardRegion: {
+ flexShrink: 0,
+ width: "100%",
+ },
+ card: {
+ gap: 10,
+ paddingHorizontal: 14,
+ paddingTop: 12,
+ paddingBottom: 14,
+ },
+ cardHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ marginBottom: 4,
+ },
+ cardTitleBlock: {
+ flex: 1,
+ gap: 6,
+ },
+ bone: {
+ backgroundColor: SKELETON_COLOR,
+ },
+});
diff --git a/components/explore/explore-feed.tsx b/components/explore/explore-feed.tsx
index 09a87af..396112b 100644
--- a/components/explore/explore-feed.tsx
+++ b/components/explore/explore-feed.tsx
@@ -1,6 +1,9 @@
import { CountryFeedPage } from "@/components/explore/country-feed-page";
import { ExploreTopBar } from "@/components/explore/explore-top-bar";
-import { getExploreHeaderContentHeight } from "@/constants/explore-feed-layout";
+import {
+ EXPLORE_FEED_BODY_BG,
+ getExploreHeroTopInset,
+} from "@/constants/explore-feed-layout";
import { getCountryImages } from "@/lib/format-country";
import { prefetchCountryProfiles } from "@/lib/prefetch-country-profiles";
import { useCountryFeedStore } from "@/store/use-country-feed-store";
@@ -42,9 +45,13 @@ export function ExploreFeed() {
const error = useCountryFeedStore((s) => s.error);
const feedListKey = `${discoveryMode}-${selectedRegion ?? "for-you"}-${focusEpoch}`;
- const headerContentInset = getExploreHeaderContentHeight(insets.top, {
- hereMode: discoveryMode === "here",
- });
+ const heroTopInset = useMemo(
+ () =>
+ getExploreHeroTopInset(insets.top, {
+ hereMode: discoveryMode === "here",
+ }),
+ [discoveryMode, insets.top],
+ );
const currentCountry = countries[currentIndex];
const currentHeroImages = useMemo(
() => (currentCountry ? getCountryImages(currentCountry) : []),
@@ -68,7 +75,7 @@ export function ExploreFeed() {
useEffect(() => {
hasSyncedInitialScrollRef.current = false;
- }, [selectedRegion, discoveryMode]);
+ }, [feedListKey]);
// Only scroll programmatically when restoring a non-zero index (e.g. layout).
// Search/home focus remounts the list at index 0 — never scroll the feed to a deep index.
@@ -86,7 +93,7 @@ export function ExploreFeed() {
scrollToCurrentIndex(animated);
}, [currentIndex, countries.length, pageHeight, scrollToCurrentIndex]);
- const onViewableItemsChanged = useRef(
+ const onViewableItemsChanged = useCallback(
({ viewableItems }: { viewableItems: ViewToken[] }) => {
const first = viewableItems[0];
if (first?.index == null) return;
@@ -112,7 +119,8 @@ export function ExploreFeed() {
void loadMoreFeed();
}
},
- ).current;
+ [loadMoreFeed, setCurrentIndex],
+ );
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
@@ -131,12 +139,12 @@ export function ExploreFeed() {
),
- [currentIndex, headerContentInset, pageHeight],
+ [currentIndex, heroTopInset, pageHeight],
);
const keyExtractor = useCallback((item: Country) => item.name, []);
@@ -242,11 +250,11 @@ const styles = StyleSheet.create({
},
feedBody: {
flex: 1,
- backgroundColor: "#0b132b",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
},
list: {
flex: 1,
- backgroundColor: "transparent",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
@@ -258,7 +266,7 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "rgba(11, 19, 43, 0.92)",
+ backgroundColor: "rgba(7, 14, 31, 0.92)",
paddingHorizontal: 32,
gap: 12,
},
diff --git a/components/explore/explore-hero-header-backdrop.tsx b/components/explore/explore-hero-header-backdrop.tsx
index a9ad8c5..3c74bda 100644
--- a/components/explore/explore-hero-header-backdrop.tsx
+++ b/components/explore/explore-hero-header-backdrop.tsx
@@ -2,6 +2,12 @@ import { BlurView } from "expo-blur";
import { Platform, StyleSheet, View } from "react-native";
import { CountryImage } from "@/components/explore/country-image";
+import {
+ EXPLORE_FEED_BODY_BG,
+ EXPLORE_FEED_HEADER_BACKDROP_GRADIENT,
+ EXPLORE_FEED_HEADER_BACKDROP_TINT,
+ EXPLORE_FEED_HEADER_BACKDROP_WEB_FALLBACK,
+} from "@/constants/explore-feed-layout";
type GradientViewStyle = {
experimental_backgroundImage: string;
@@ -15,12 +21,6 @@ type ExploreHeroHeaderBackdropProps = {
};
const BACKDROP_BLUR_INTENSITY = 56;
-const BACKDROP_TINT = "rgba(5, 10, 24, 0.52)";
-const BACKDROP_WEB_FALLBACK = "rgba(5, 10, 24, 0.9)";
-
-/** Heavy top band for header chrome legibility over bright photos. */
-const BACKDROP_TOP_GRADIENT =
- "linear-gradient(to bottom, rgba(3, 6, 16, 0.88) 0%, rgba(5, 10, 24, 0.68) 32%, rgba(11, 19, 43, 0.34) 58%, rgba(11, 19, 43, 0.08) 82%, rgba(11, 19, 43, 0) 100%)";
/** Blurred hero imagery fixed behind the explore header chrome. */
export function ExploreHeroHeaderBackdrop({
@@ -53,7 +53,7 @@ export function ExploreHeroHeaderBackdrop({
style={[
styles.topGradient,
{
- experimental_backgroundImage: BACKDROP_TOP_GRADIENT,
+ experimental_backgroundImage: EXPLORE_FEED_HEADER_BACKDROP_GRADIENT,
} satisfies GradientViewStyle,
]}
/>
@@ -68,7 +68,7 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
overflow: "hidden",
- backgroundColor: "#0b132b",
+ backgroundColor: EXPLORE_FEED_BODY_BG,
},
image: {
...StyleSheet.absoluteFillObject,
@@ -76,11 +76,11 @@ const styles = StyleSheet.create({
},
webFallback: {
...StyleSheet.absoluteFillObject,
- backgroundColor: BACKDROP_WEB_FALLBACK,
+ backgroundColor: EXPLORE_FEED_HEADER_BACKDROP_WEB_FALLBACK,
},
tint: {
...StyleSheet.absoluteFillObject,
- backgroundColor: BACKDROP_TINT,
+ backgroundColor: EXPLORE_FEED_HEADER_BACKDROP_TINT,
},
topGradient: {
...StyleSheet.absoluteFillObject,
diff --git a/components/explore/explore-loading.tsx b/components/explore/explore-loading.tsx
index e14e006..4ebc25b 100644
--- a/components/explore/explore-loading.tsx
+++ b/components/explore/explore-loading.tsx
@@ -1,13 +1,18 @@
import { ActivityIndicator, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { EXPLORE_FEED_BODY_BG } from "@/constants/explore-feed-layout";
+
export function ExploreLoading() {
const insets = useSafeAreaInsets();
return (
Loading countries…
diff --git a/components/explore/explore-top-bar.tsx b/components/explore/explore-top-bar.tsx
index da16973..574d3c0 100644
--- a/components/explore/explore-top-bar.tsx
+++ b/components/explore/explore-top-bar.tsx
@@ -2,12 +2,15 @@ import { useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import {
+ ExploreCountryMoreMenus,
+ getExploreHasCustomSort,
+} from "@/components/explore/explore-country-more-menus";
import { ExploreFeedMenuSheet } from "@/components/explore/explore-feed-menu-sheet";
import { ExploreHeroHeaderBackdrop } from "@/components/explore/explore-hero-header-backdrop";
import { ExploreTopChromeScrim } from "@/components/explore/explore-top-chrome-scrim";
import {
WORLDLOOP_HEADER_BOTTOM_PADDING,
- WORLDLOOP_HEADER_HORIZONTAL_PADDING,
WORLDLOOP_HEADER_TOP_PADDING,
WorldLoopHeader,
} from "@/components/worldloop-header";
@@ -17,6 +20,7 @@ import {
CULTURE_CHROME_TOUCH_SIZE,
} from "@/constants/culture-chrome";
import {
+ EXPLORE_HEADER_HORIZONTAL_PADDING,
EXPLORE_HEADER_OVERLAY_BOTTOM_PADDING,
getExploreHeaderChromeHeight,
} from "@/constants/explore-feed-layout";
@@ -50,6 +54,13 @@ export function ExploreTopBar({
);
const [isFeedMenuOpen, setIsFeedMenuOpen] = useState(false);
+ const [isMoreMenuOpen, setIsMoreMenuOpen] = useState(false);
+ const countries = useCountryFeedStore((s) => s.countries);
+ const currentIndex = useCountryFeedStore((s) => s.currentIndex);
+ const sortField = useCountryFeedStore((s) => s.sortField);
+ const sortOrder = useCountryFeedStore((s) => s.sortOrder);
+ const currentCountry = countries[currentIndex];
+ const hasCustomSort = getExploreHasCustomSort(sortField, sortOrder);
const hereScopeLabel = focusedRegion
? continentDisplayLabel(focusedRegion)
@@ -65,7 +76,7 @@ export function ExploreTopBar({
return (
{hasHeroBackdrop ? (
openSearch()}
searchActive={isSearchOpen}
+ showMore
+ onMorePress={() => setIsMoreMenuOpen(true)}
+ moreActive={hasCustomSort || isMoreMenuOpen}
inactiveColor={EXPLORE_HEADER_INACTIVE_COLOR}
+ brandFontFamily="Poppins-Bold"
rowHeight={CULTURE_CHROME_TOUCH_SIZE}
sideSlotWidth={CULTURE_CHROME_TOUCH_SIZE}
brandFontSize={CULTURE_CHROME_TITLE_SIZE}
iconSize={CULTURE_CHROME_ICON_SIZE}
- searchIconSize={CULTURE_CHROME_ICON_SIZE}
+ horizontalPadding={EXPLORE_HEADER_HORIZONTAL_PADDING}
/>
{discoveryMode === "here" ? (
@@ -109,6 +124,12 @@ export function ExploreTopBar({
visible={isFeedMenuOpen}
onClose={() => setIsFeedMenuOpen(false)}
/>
+
+ setIsMoreMenuOpen(false)}
+ />
);
@@ -123,7 +144,7 @@ const styles = StyleSheet.create({
zIndex: 10,
},
hereSubtitle: {
- paddingHorizontal: WORLDLOOP_HEADER_HORIZONTAL_PADDING,
+ paddingHorizontal: EXPLORE_HEADER_HORIZONTAL_PADDING,
fontSize: 11,
lineHeight: 14,
fontFamily: "Poppins-Regular",
diff --git a/components/explore/explore-top-chrome-scrim.tsx b/components/explore/explore-top-chrome-scrim.tsx
index f1104f4..fb89779 100644
--- a/components/explore/explore-top-chrome-scrim.tsx
+++ b/components/explore/explore-top-chrome-scrim.tsx
@@ -1,6 +1,12 @@
import { BlurView } from "expo-blur";
import { Platform, StyleSheet, View } from "react-native";
+import {
+ EXPLORE_FEED_CHROME_BASE_TINT,
+ EXPLORE_FEED_CHROME_SCRIM_FALLBACK,
+ EXPLORE_FEED_CHROME_TOP_GRADIENT,
+} from "@/constants/explore-feed-layout";
+
type GradientViewStyle = {
experimental_backgroundImage: string;
};
@@ -10,11 +16,6 @@ type ExploreTopChromeScrimProps = {
};
const HEADER_SCRIM_BLUR_INTENSITY = 52;
-const HEADER_SCRIM_WEB_FALLBACK = "rgba(5, 10, 24, 0.88)";
-
-/** Dense top band keeps logo and icons legible on bright hero photos. */
-const HEADER_SCRIM_TOP_GRADIENT =
- "linear-gradient(to bottom, rgba(3, 6, 16, 0.82) 0%, rgba(5, 10, 24, 0.62) 38%, rgba(11, 19, 43, 0.28) 68%, rgba(11, 19, 43, 0) 100%)";
/** Frosted fade so header chrome stays readable without blocking the hero image. */
export function ExploreTopChromeScrim({ height }: ExploreTopChromeScrimProps) {
@@ -34,7 +35,7 @@ export function ExploreTopChromeScrim({ height }: ExploreTopChromeScrimProps) {
style={[
styles.fadeOverlay,
{
- experimental_backgroundImage: HEADER_SCRIM_TOP_GRADIENT,
+ experimental_backgroundImage: EXPLORE_FEED_CHROME_TOP_GRADIENT,
} satisfies GradientViewStyle,
]}
/>
@@ -52,11 +53,11 @@ const styles = StyleSheet.create({
},
webFallback: {
...StyleSheet.absoluteFillObject,
- backgroundColor: HEADER_SCRIM_WEB_FALLBACK,
+ backgroundColor: EXPLORE_FEED_CHROME_SCRIM_FALLBACK,
},
baseTint: {
...StyleSheet.absoluteFillObject,
- backgroundColor: "rgba(3, 6, 16, 0.28)",
+ backgroundColor: EXPLORE_FEED_CHROME_BASE_TINT,
},
fadeOverlay: {
...StyleSheet.absoluteFillObject,
diff --git a/components/explore/glass-icon-button.tsx b/components/explore/glass-icon-button.tsx
index fb2dbd3..a65aa1d 100644
--- a/components/explore/glass-icon-button.tsx
+++ b/components/explore/glass-icon-button.tsx
@@ -198,17 +198,17 @@ const styles = StyleSheet.create({
borderRadius: GLASS_TOUCH_SIZE / 2,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "rgba(0, 0, 0, 0.45)",
+ backgroundColor: "rgba(0, 0, 0, 0.28)",
borderWidth: 1,
- borderColor: "rgba(255, 255, 255, 0.12)",
+ borderColor: "rgba(255, 255, 255, 0.08)",
},
circleActive: {
- backgroundColor: "rgba(251, 191, 36, 0.18)",
- borderColor: "rgba(251, 191, 36, 0.45)",
+ backgroundColor: "rgba(251, 191, 36, 0.12)",
+ borderColor: "rgba(0, 0, 0, 0.22)",
},
circleDisabled: {
- backgroundColor: "rgba(0, 0, 0, 0.3)",
- borderColor: "rgba(255, 255, 255, 0.08)",
+ backgroundColor: "rgba(0, 0, 0, 0.18)",
+ borderColor: "rgba(255, 255, 255, 0.06)",
},
label: {
fontSize: 11,
diff --git a/components/profile/profile-back-button.tsx b/components/profile/profile-back-button.tsx
new file mode 100644
index 0000000..31955cd
--- /dev/null
+++ b/components/profile/profile-back-button.tsx
@@ -0,0 +1,60 @@
+import { Ionicons } from "@expo/vector-icons";
+import { Pressable, StyleSheet, View } from "react-native";
+
+import { PROFILE_BACK_BUTTON_RING } from "@/constants/profile-theme";
+
+type ProfileBackButtonProps = {
+ onPress: () => void;
+ accessibilityLabel?: string;
+ /** Outer ring diameter. */
+ size?: number;
+ iconSize?: number;
+ iconColor?: string;
+};
+
+export function ProfileBackButton({
+ onPress,
+ accessibilityLabel = "Go back",
+ size = 36,
+ iconSize = 22,
+ iconColor = "#ffffff",
+}: ProfileBackButtonProps) {
+ return (
+ [styles.hitArea, pressed && styles.pressed]}
+ >
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ hitArea: {
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ ring: {
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ borderColor: PROFILE_BACK_BUTTON_RING,
+ },
+ pressed: {
+ opacity: 0.85,
+ },
+});
diff --git a/components/profile/profile-hero-backdrop.tsx b/components/profile/profile-hero-backdrop.tsx
new file mode 100644
index 0000000..c4dac1a
--- /dev/null
+++ b/components/profile/profile-hero-backdrop.tsx
@@ -0,0 +1,77 @@
+import { BlurView } from "expo-blur";
+import { Image } from "expo-image";
+import { Platform, StyleSheet, View } from "react-native";
+
+import { images } from "@/constants/images";
+import { SPACE_WEB_BLUR_FALLBACK } from "@/constants/space-theme";
+
+const PROFILE_HERO_BLUR_INTENSITY = 48;
+
+type GradientViewStyle = {
+ experimental_backgroundImage: string;
+};
+
+/** Fades hero into the solid profile background below. */
+const BOTTOM_FADE_GRADIENT =
+ "linear-gradient(to bottom, rgba(11, 19, 43, 0) 0%, rgba(11, 19, 43, 0.35) 55%, rgba(11, 19, 43, 0.92) 88%, #0b132b 100%)";
+
+type ProfileHeroBackdropProps = {
+ height: number;
+};
+
+export function ProfileHeroBackdrop({ height }: ProfileHeroBackdropProps) {
+ return (
+
+
+ {Platform.OS === "web" ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ shell: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ overflow: "hidden",
+ backgroundColor: "#0b132b",
+ },
+ image: {
+ ...StyleSheet.absoluteFillObject,
+ transform: [{ scale: 1.06 }],
+ },
+ webFallback: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: SPACE_WEB_BLUR_FALLBACK,
+ },
+ scrim: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: "rgba(11, 19, 43, 0.38)",
+ },
+ bottomFade: {
+ ...StyleSheet.absoluteFillObject,
+ },
+});
diff --git a/components/profile/profile-hero-header.tsx b/components/profile/profile-hero-header.tsx
new file mode 100644
index 0000000..0504b1b
--- /dev/null
+++ b/components/profile/profile-hero-header.tsx
@@ -0,0 +1,167 @@
+import { useUser } from "@clerk/expo";
+import { Ionicons } from "@expo/vector-icons";
+import { Image } from "expo-image";
+import { router } from "expo-router";
+import { Pressable, StyleSheet, Text, View } from "react-native";
+
+import { images } from "@/constants/images";
+import {
+ PROFILE_HERO_TOP_PADDING,
+ PROFILE_TEXT_SUBTITLE,
+} from "@/constants/profile-theme";
+import type { ProfileInlineStats } from "@/hooks/use-profile-stats";
+
+const DEFAULT_BIO = "Exploring the world, one destination at a time.";
+
+type ProfileHeroHeaderProps = {
+ stats: ProfileInlineStats;
+ onEditPress?: () => void;
+};
+
+export function ProfileHeroHeader({
+ stats,
+ onEditPress,
+}: ProfileHeroHeaderProps) {
+ const { user } = useUser();
+
+ const displayName =
+ user?.fullName ?? user?.firstName ?? user?.username ?? "Traveler";
+
+ const emailLocal = user?.primaryEmailAddress?.emailAddress?.split("@")[0];
+ const handle = user?.username
+ ? `@${user.username}`
+ : emailLocal
+ ? `@${emailLocal}`
+ : "@traveler";
+
+ const bio =
+ typeof user?.unsafeMetadata?.bio === "string" && user.unsafeMetadata.bio
+ ? user.unsafeMetadata.bio
+ : DEFAULT_BIO;
+
+ const avatarUri = user?.imageUrl ?? null;
+
+ const handleEdit = () => {
+ if (onEditPress) {
+ onEditPress();
+ return;
+ }
+ router.push("/(tabs)/profile/settings");
+ };
+
+ return (
+
+
+
+
+
+
+ {displayName}
+
+
+ {handle}
+
+
+ {bio}
+
+
+
+
+
+
+
+
+
+
+ [
+ styles.editButton,
+ pressed && styles.editButtonPressed,
+ ]}
+ >
+
+ Edit profile
+
+
+ );
+}
+
+function InlineStat({ value, label }: { value: number; label: string }) {
+ return (
+
+
+ {value}
+
+
+ {label}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingTop: PROFILE_HERO_TOP_PADDING,
+ paddingBottom: 24,
+ },
+ avatarFrame: {
+ width: 96,
+ height: 96,
+ borderRadius: 48,
+ borderWidth: 1.5,
+ borderColor: "rgba(255, 255, 255, 0.22)",
+ overflow: "hidden",
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
+ marginBottom: 12,
+ },
+ avatar: {
+ width: "100%",
+ height: "100%",
+ },
+ statsGroup: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginTop: 16,
+ },
+ inlineStat: {
+ alignItems: "center",
+ minWidth: 64,
+ },
+ statDivider: {
+ width: 1,
+ height: 28,
+ marginHorizontal: 16,
+ backgroundColor: "rgba(255, 255, 255, 0.16)",
+ },
+ editButton: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ marginTop: 16,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ borderRadius: 12,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.08)",
+ },
+ editButtonPressed: {
+ opacity: 0.85,
+ },
+});
diff --git a/components/profile/profile-log-out-button.tsx b/components/profile/profile-log-out-button.tsx
new file mode 100644
index 0000000..686c2ff
--- /dev/null
+++ b/components/profile/profile-log-out-button.tsx
@@ -0,0 +1,53 @@
+import { useClerk } from "@clerk/expo";
+import { Ionicons } from "@expo/vector-icons";
+import { router } from "expo-router";
+import { Alert, Pressable, StyleSheet, Text } from "react-native";
+
+export function ProfileLogOutButton() {
+ const { signOut } = useClerk();
+
+ const handleLogOut = () => {
+ Alert.alert("Log out", "Are you sure you want to log out?", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Log out",
+ style: "destructive",
+ onPress: () => {
+ void (async () => {
+ await signOut();
+ router.replace("/onboarding");
+ })();
+ },
+ },
+ ]);
+ };
+
+ return (
+ [styles.button, pressed && styles.buttonPressed]}
+ >
+
+ Log out
+
+ );
+}
+
+const styles = StyleSheet.create({
+ button: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 10,
+ marginHorizontal: 16,
+ marginTop: 16,
+ paddingVertical: 16,
+ borderRadius: 16,
+ backgroundColor: "#1a2235",
+ },
+ buttonPressed: {
+ opacity: 0.88,
+ },
+});
diff --git a/components/profile/profile-nav-row.tsx b/components/profile/profile-nav-row.tsx
new file mode 100644
index 0000000..a6e4e53
--- /dev/null
+++ b/components/profile/profile-nav-row.tsx
@@ -0,0 +1,290 @@
+import { Ionicons } from "@expo/vector-icons";
+import * as Haptics from "expo-haptics";
+import { Image } from "expo-image";
+import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
+
+import { FlagBadge } from "@/components/explore/flag-badge";
+import { images } from "@/constants/images";
+import {
+ PROFILE_ICON,
+ PROFILE_ICON_RING,
+ PROFILE_TEXT_SUBTITLE,
+} from "@/constants/profile-theme";
+import type { VisitedCountryPreview } from "@/hooks/use-profile-stats";
+
+type ProfileNavRowProps = {
+ icon: keyof typeof Ionicons.glyphMap;
+ title: string;
+ subtitle: string;
+ onPress: () => void;
+ trailing?: "thumbnail" | "flags" | "map" | "none";
+ thumbnailUri?: string | null;
+ visitedCountries?: VisitedCountryPreview[];
+ totalVisited?: number;
+};
+
+export function ProfileNavRow({
+ icon,
+ title,
+ subtitle,
+ onPress,
+ trailing = "none",
+ thumbnailUri,
+ visitedCountries = [],
+ totalVisited = 0,
+}: ProfileNavRowProps) {
+ const handlePress = () => {
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onPress();
+ };
+
+ return (
+ [styles.card, pressed && styles.cardPressed]}
+ >
+
+
+
+
+
+
+ {title}
+ {subtitle}
+
+
+
+ {trailing === "thumbnail" ? (
+
+ ) : trailing === "flags" ? (
+
+ ) : trailing === "map" ? (
+
+ ) : null}
+
+
+
+
+ );
+}
+
+function Thumbnail({ uri }: { uri?: string | null }) {
+ return (
+
+
+
+
+
+ );
+}
+
+function MapThumbnail() {
+ return (
+
+
+
+
+ {MAP_PREVIEW_PINS.map((pin) => (
+
+ ))}
+
+
+ );
+}
+
+const MAP_PREVIEW_PINS = [
+ { id: "na", top: 28, left: 18 },
+ { id: "eu", top: 24, left: 48 },
+ { id: "af", top: 46, left: 52 },
+ { id: "as", top: 34, left: 72 },
+ { id: "au", top: 62, left: 82 },
+] as const;
+
+function FlagStrip({
+ countries,
+ totalVisited,
+}: {
+ countries: VisitedCountryPreview[];
+ totalVisited: number;
+}) {
+ const visible = countries.slice(0, 3);
+ const remainder = Math.max(0, totalVisited - visible.length);
+
+ return (
+
+ {visible.map((country) => (
+
+
+
+ ))}
+ {remainder > 0 ? (
+
+
+ +{remainder}
+
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: 16,
+ backgroundColor: "#1a2235",
+ paddingHorizontal: 14,
+ paddingVertical: 14,
+ },
+ cardPressed: {
+ opacity: 0.88,
+ },
+ row: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ gap: 12,
+ },
+ iconBox: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ borderColor: PROFILE_ICON_RING,
+ },
+ textBlock: {
+ flex: 1,
+ minWidth: 0,
+ paddingTop: 1,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: "500",
+ lineHeight: 20,
+ color: "#ffffff",
+ },
+ subtitle: {
+ marginTop: 2,
+ fontSize: 13,
+ fontWeight: "400",
+ lineHeight: 18,
+ color: PROFILE_TEXT_SUBTITLE,
+ },
+ trailing: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ alignSelf: "center",
+ },
+ thumbnailImage: {
+ width: "100%",
+ height: "100%",
+ },
+ savedThumbnailFrame: {
+ width: 60,
+ height: 44,
+ borderRadius: 10,
+ overflow: "hidden",
+ },
+ mapThumbnailFrame: {
+ width: 72,
+ height: 44,
+ borderRadius: 10,
+ overflow: "hidden",
+ backgroundColor: "#080c1c",
+ },
+ previewCell: {
+ borderRadius: 12,
+ // padding: 2,
+ alignItems: "center",
+ justifyContent: "center",
+ overflow: "hidden",
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.05)",
+ },
+ mapImageZoomed: {
+ ...StyleSheet.absoluteFillObject,
+ transform: [{ scale: 1.8 }],
+ overflow: "hidden",
+ },
+ mapTint: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: "rgba(8, 12, 28, 0.3)",
+ },
+ mapPin: {
+ position: "absolute",
+ width: 2,
+ height: 2,
+ marginTop: -2,
+ marginLeft: -2,
+ borderRadius: 2,
+ backgroundColor: "#fbbf24",
+ },
+ flagStrip: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 0,
+ },
+ flagCell: {
+ width: 28,
+ height: 24,
+ borderRadius: 6,
+ alignItems: "center",
+ justifyContent: "center",
+ overflow: "hidden",
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.05)",
+ },
+ moreBadge: {
+ width: 28,
+ height: 24,
+ borderRadius: 6,
+ overflow: "hidden",
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.1)",
+ },
+ moreBadgeText: {
+ fontSize: 8,
+ fontWeight: "500",
+ color: "#ffffff",
+ textAlign: "center",
+ lineHeight: 22,
+ includeFontPadding: false,
+ ...(Platform.OS === "android"
+ ? { textAlignVertical: "center" as const }
+ : {}),
+ },
+});
diff --git a/components/profile/profile-settings-header.tsx b/components/profile/profile-settings-header.tsx
new file mode 100644
index 0000000..6617eae
--- /dev/null
+++ b/components/profile/profile-settings-header.tsx
@@ -0,0 +1,104 @@
+import { useUser } from "@clerk/expo";
+import { Ionicons } from "@expo/vector-icons";
+import { Image } from "expo-image";
+import { StyleSheet, Text, View } from "react-native";
+
+import { images } from "@/constants/images";
+import {
+ PROFILE_EDIT_BADGE_BG,
+ PROFILE_EDIT_BADGE_ICON,
+} from "@/constants/profile-theme";
+
+const DEFAULT_LOCATION = "San Francisco, USA";
+
+export function ProfileSettingsHeader() {
+ const { user } = useUser();
+
+ const displayName =
+ user?.fullName ?? user?.firstName ?? user?.username ?? "Traveler";
+
+ const email =
+ user?.primaryEmailAddress?.emailAddress ?? "alex.morgan@example.com";
+
+ const location =
+ typeof user?.unsafeMetadata?.location === "string" &&
+ user.unsafeMetadata.location
+ ? user.unsafeMetadata.location
+ : DEFAULT_LOCATION;
+
+ const avatarUri = user?.imageUrl ?? null;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {displayName}
+ {email}
+
+
+ {location}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ root: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 16,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ avatarWrap: {
+ width: 68,
+ height: 68,
+ },
+ avatar: {
+ width: 68,
+ height: 68,
+ borderRadius: 34,
+ borderWidth: 2,
+ borderColor: "rgba(255, 255, 255, 0.18)",
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
+ },
+ editBadge: {
+ position: "absolute",
+ right: -2,
+ bottom: -2,
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: PROFILE_EDIT_BADGE_BG,
+ borderWidth: 2,
+ borderColor: "#0b132b",
+ },
+ info: {
+ flex: 1,
+ minWidth: 0,
+ },
+ locationRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 4,
+ marginTop: 6,
+ },
+});
diff --git a/components/profile/profile-settings-row.tsx b/components/profile/profile-settings-row.tsx
new file mode 100644
index 0000000..c654130
--- /dev/null
+++ b/components/profile/profile-settings-row.tsx
@@ -0,0 +1,114 @@
+import { Ionicons } from "@expo/vector-icons";
+import { Pressable, StyleSheet, Switch, Text, View } from "react-native";
+
+import {
+ PROFILE_ICON,
+ PROFILE_SWITCH_TRACK_ON,
+} from "@/constants/profile-theme";
+
+type ProfileSettingsRowProps = {
+ icon: keyof typeof Ionicons.glyphMap;
+ label: string;
+ value?: string;
+ showChevron?: boolean;
+ showToggle?: boolean;
+ toggleValue?: boolean;
+ onToggle?: (value: boolean) => void;
+ onPress?: () => void;
+ isLast?: boolean;
+};
+
+export function ProfileSettingsRow({
+ icon,
+ label,
+ value,
+ showChevron = true,
+ showToggle = false,
+ toggleValue = false,
+ onToggle,
+ onPress,
+ isLast = false,
+}: ProfileSettingsRowProps) {
+ const content = (
+ <>
+
+
+ {label}
+
+
+
+ {value ? (
+ {value}
+ ) : null}
+ {showToggle ? (
+
+ ) : showChevron ? (
+
+ ) : null}
+
+ >
+ );
+
+ if (showToggle) {
+ return (
+ {content}
+ );
+ }
+
+ return (
+ [
+ styles.row,
+ !isLast && styles.rowBorder,
+ pressed && styles.rowPressed,
+ ]}
+ >
+ {content}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ minHeight: 56,
+ },
+ rowBorder: {
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: "rgba(255, 255, 255, 0.08)",
+ },
+ rowPressed: {
+ opacity: 0.88,
+ },
+ left: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ flex: 1,
+ },
+ right: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 4,
+ },
+});
diff --git a/components/profile/profile-stats-bar.tsx b/components/profile/profile-stats-bar.tsx
new file mode 100644
index 0000000..e508618
--- /dev/null
+++ b/components/profile/profile-stats-bar.tsx
@@ -0,0 +1,65 @@
+import { Ionicons } from "@expo/vector-icons";
+import { StyleSheet, Text, View } from "react-native";
+
+import { PROFILE_ICON } from "@/constants/profile-theme";
+
+type ProfileStatsBarProps = {
+ countries: number;
+ saved: number;
+ photos: number;
+};
+
+export function ProfileStatsBar({
+ countries,
+ saved,
+ photos,
+}: ProfileStatsBarProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function StatColumn({
+ icon,
+ value,
+ label,
+}: {
+ icon: keyof typeof Ionicons.glyphMap;
+ value: number;
+ label: string;
+}) {
+ return (
+
+
+ {value}
+ {label}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 16,
+ backgroundColor: "#1a2235",
+ paddingVertical: 18,
+ paddingHorizontal: 8,
+ marginHorizontal: 16,
+ },
+ column: {
+ flex: 1,
+ alignItems: "center",
+ },
+ divider: {
+ width: 1,
+ height: 56,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ },
+});
diff --git a/components/worldloop-header.tsx b/components/worldloop-header.tsx
index 886cf6a..4aedb01 100644
--- a/components/worldloop-header.tsx
+++ b/components/worldloop-header.tsx
@@ -1,3 +1,4 @@
+import { ProfileBackButton } from "@/components/profile/profile-back-button";
import { Ionicons } from "@expo/vector-icons";
import { Pressable, StyleSheet, Text, View } from "react-native";
@@ -21,10 +22,20 @@ export function getWorldLoopHeaderHeight(safeAreaTop: number) {
}
type WorldLoopHeaderProps = {
- onMenuPress: () => void;
+ onMenuPress?: () => void;
menuActive?: boolean;
- onSearchPress: () => void;
+ onSearchPress?: () => void;
searchActive?: boolean;
+ /** Center title — defaults to WorldLoop. */
+ title?: string;
+ /** When false, left slot is empty (keeps title centered). */
+ showMenu?: boolean;
+ /** When false, right slot is empty (keeps title centered). */
+ showSearch?: boolean;
+ /** When true, left slot shows a back chevron instead of menu/empty. */
+ showBack?: boolean;
+ onBackPress?: () => void;
+ backAccessibilityLabel?: string;
inactiveColor?: string;
brandFontFamily?: string;
/** Row height — use 48+ on immersive overlays for 44pt touch targets. */
@@ -37,6 +48,13 @@ type WorldLoopHeaderProps = {
menuAccessibilityHint?: string;
searchAccessibilityLabel?: string;
searchAccessibilityHint?: string;
+ showMore?: boolean;
+ onMorePress?: () => void;
+ moreActive?: boolean;
+ moreAccessibilityLabel?: string;
+ moreAccessibilityHint?: string;
+ /** Override horizontal inset for icon row — defaults to 16. */
+ horizontalPadding?: number;
};
export function WorldLoopHeader({
@@ -44,6 +62,12 @@ export function WorldLoopHeader({
menuActive = false,
onSearchPress,
searchActive = false,
+ title = "WorldLoop",
+ showMenu = true,
+ showSearch = true,
+ showBack = false,
+ onBackPress,
+ backAccessibilityLabel = "Go back",
inactiveColor = WORLDLOOP_HEADER_MUTED_COLOR,
brandFontFamily = "Poppins-Medium",
rowHeight = WORLDLOOP_HEADER_ROW_HEIGHT,
@@ -55,36 +79,78 @@ export function WorldLoopHeader({
menuAccessibilityHint = "Opens For You, Here, and continent filters",
searchAccessibilityLabel = "Search countries",
searchAccessibilityHint = "Opens country search",
+ showMore = false,
+ onMorePress,
+ moreActive = false,
+ moreAccessibilityLabel = "More actions",
+ moreAccessibilityHint = "Opens share, sort, and other country actions",
+ horizontalPadding = WORLDLOOP_HEADER_HORIZONTAL_PADDING,
}: WorldLoopHeaderProps) {
+ const actionIconSize = iconSize;
+ const leftSlotWidth = sideSlotWidth;
+ const hasDualRightActions = showMore && showSearch;
+ const rightActionWidth = hasDualRightActions
+ ? actionIconSize + 8
+ : sideSlotWidth;
+ const rightSlotWidth = hasDualRightActions
+ ? rightActionWidth * 2
+ : sideSlotWidth;
+ const titleSideInsetLeft = horizontalPadding + leftSlotWidth;
+ const titleSideInsetRight = horizontalPadding + rightSlotWidth;
+ const centerTitleInRow = leftSlotWidth !== rightSlotWidth;
+
return (
-
-
- {({ pressed }) => (
-
-
-
- )}
-
+
+ {showBack ? (
+
+ {})}
+ accessibilityLabel={backAccessibilityLabel}
+ size={36}
+ iconSize={iconSize + 2}
+ iconColor={inactiveColor}
+ />
+
+ ) : showMenu ? (
+
+ {({ pressed }) => (
+
+
+
+ )}
+
+ ) : (
+
+ )}
- WorldLoop
+ {title}
- [
- styles.sideSlot,
- { width: sideSlotWidth, height: rowHeight },
- pressed && styles.pressed,
- ]}
- >
+ {showSearch || showMore ? (
-
+ {showSearch ? (
+ [
+ styles.rightActionButton,
+ { width: rightActionWidth },
+ pressed && styles.pressed,
+ ]}
+ >
+ {({ pressed }) => (
+
+
+
+ )}
+
+ ) : null}
+
+ {showMore ? (
+ [
+ styles.rightActionButton,
+ { width: rightActionWidth },
+ pressed && styles.pressed,
+ ]}
+ >
+ {({ pressed }) => (
+
+
+
+ )}
+
+ ) : null}
-
+ ) : (
+
+ )}
);
}
@@ -138,17 +262,22 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
- paddingHorizontal: WORLDLOOP_HEADER_HORIZONTAL_PADDING,
},
sideSlot: {
alignItems: "center",
justifyContent: "center",
},
- headerIconBox: {
+ rightActions: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ },
+ rightActionButton: {
+ height: "100%",
alignItems: "center",
justifyContent: "center",
},
- searchIconBox: {
+ headerIconBox: {
alignItems: "center",
justifyContent: "center",
},
diff --git a/constants/auth-theme.ts b/constants/auth-theme.ts
index c8c52fb..07ea3a8 100644
--- a/constants/auth-theme.ts
+++ b/constants/auth-theme.ts
@@ -31,8 +31,10 @@ export const AUTH_COLORS = {
tabActiveText: "#FFFFFF",
dividerLine: "rgba(255, 255, 255, 0.14)",
socialBg: "rgba(255, 255, 255, 0.06)",
+ socialBgPressed: "rgba(255, 255, 255, 0.16)",
/** Outlined secondary control — matches register social buttons */
socialBorder: "rgba(255, 255, 255, 0.14)",
+ socialBorderPressed: "rgba(255, 255, 255, 0.38)",
socialText: "#FFFFFF",
google: "#EA4335",
facebook: "#1877F2",
diff --git a/constants/culture-chrome.ts b/constants/culture-chrome.ts
index b3faaaf..9b49cfc 100644
--- a/constants/culture-chrome.ts
+++ b/constants/culture-chrome.ts
@@ -3,3 +3,4 @@ export const CULTURE_CHROME_ICON_SIZE = 26;
export const CULTURE_CHROME_TOUCH_SIZE = 44;
export const CULTURE_CHROME_TITLE_SIZE = 18;
export const CULTURE_CHROME_RAIL_GAP = 20;
+export const CULTURE_HEADER_HORIZONTAL_PADDING = 8;
diff --git a/constants/explore-feed-layout.ts b/constants/explore-feed-layout.ts
index 188efa7..3688278 100644
--- a/constants/explore-feed-layout.ts
+++ b/constants/explore-feed-layout.ts
@@ -1,14 +1,40 @@
import { WORLDLOOP_HEADER_TOP_PADDING } from "@/components/worldloop-header";
import { CULTURE_CHROME_TOUCH_SIZE } from "@/constants/culture-chrome";
+/**
+ * Content panel below the hero — slightly lifted from tab bar (#050a18) so
+ * photography hands off to UI with a touch more tonal separation.
+ */
+export const EXPLORE_FEED_CARD_SURFACE = "#070e1f";
+
+/** Explore feed body + hero chrome — matches the country info card. */
+export const EXPLORE_FEED_BODY_BG = EXPLORE_FEED_CARD_SURFACE;
+
+/** Header scrim tints — derived from the card surface. */
+export const EXPLORE_FEED_CHROME_SCRIM_FALLBACK = "rgba(7, 14, 31, 0.88)";
+export const EXPLORE_FEED_CHROME_BASE_TINT = "rgba(7, 14, 31, 0.28)";
+export const EXPLORE_FEED_CHROME_TOP_GRADIENT =
+ "linear-gradient(to bottom, rgba(7, 14, 31, 0.82) 0%, rgba(7, 14, 31, 0.62) 38%, rgba(7, 14, 31, 0.24) 68%, rgba(7, 14, 31, 0) 100%)";
+
+export const EXPLORE_FEED_HEADER_BACKDROP_TINT = "rgba(7, 14, 31, 0.52)";
+export const EXPLORE_FEED_HEADER_BACKDROP_WEB_FALLBACK = "rgba(7, 14, 31, 0.9)";
+export const EXPLORE_FEED_HEADER_BACKDROP_GRADIENT =
+ "linear-gradient(to bottom, rgba(7, 14, 31, 0.88) 0%, rgba(7, 14, 31, 0.68) 32%, rgba(7, 14, 31, 0.28) 58%, rgba(7, 14, 31, 0.08) 82%, rgba(7, 14, 31, 0) 100%)";
+
/** Outer radius for the stacked hero + info card unit. */
export const EXPLORE_FEED_SURFACE_RADIUS = 14;
/** Extra fade below header chrome where blurred hero blends into the sharp feed. */
-export const EXPLORE_HEADER_SCRIM_EXTRA = 14;
+export const EXPLORE_HEADER_SCRIM_EXTRA = 6;
/** Bottom padding used when the explore header overlays the feed. */
-export const EXPLORE_HEADER_OVERLAY_BOTTOM_PADDING = 2;
+export const EXPLORE_HEADER_OVERLAY_BOTTOM_PADDING = 0;
+
+/** Tighter edge inset for explore header icons vs default WorldLoop chrome (16). */
+export const EXPLORE_HEADER_HORIZONTAL_PADDING = 8;
+
+/** Pull hero imagery under the floating header to remove the visible seam. */
+export const EXPLORE_HEADER_HERO_OVERLAP = 8;
/** Height of the "Here · …" subtitle row under the header. */
export const EXPLORE_HERE_SUBTITLE_HEIGHT = 14;
@@ -27,6 +53,18 @@ export function getExploreHeaderContentHeight(
);
}
+/** Top inset for feed hero imagery — overlaps header chrome slightly. */
+export function getExploreHeroTopInset(
+ safeAreaTop: number,
+ options?: { hereMode?: boolean },
+) {
+ return Math.max(
+ safeAreaTop,
+ getExploreHeaderContentHeight(safeAreaTop, options) -
+ EXPLORE_HEADER_HERO_OVERLAP,
+ );
+}
+
/** Full frosted header band including the blend fade into the feed. */
export function getExploreHeaderChromeHeight(
safeAreaTop: number,
@@ -38,12 +76,6 @@ export function getExploreHeaderChromeHeight(
);
}
-/**
- * Content panel below the hero — slightly lifted from tab bar (#050a18) so
- * photography hands off to UI with a touch more tonal separation.
- */
-export const EXPLORE_FEED_CARD_SURFACE = "#070e1f";
-
/** Horizontal padding inside the country info card. */
export const EXPLORE_FEED_CARD_HORIZONTAL_PADDING = 14;
diff --git a/constants/images.ts b/constants/images.ts
index 783a7e6..3be2ecb 100644
--- a/constants/images.ts
+++ b/constants/images.ts
@@ -1,4 +1,5 @@
import authBackground from "@/assets/images/background-main.png";
+import backgroundUi from "@/assets/images/background-ui.png";
import earthMap from "@/assets/images/earth-map.jpg";
import earthTopography from "@/assets/images/earth-topography.jpg";
import googleLogo from "@/assets/images/google-logo.png";
@@ -10,6 +11,8 @@ export const images = {
googleLogo,
worldloopIcon,
profileAvatar,
+ profileHero: backgroundUi,
+ backgroundUi,
earthMap,
earthTopography,
onboardingDiscoverPoster,
diff --git a/constants/profile-theme.ts b/constants/profile-theme.ts
new file mode 100644
index 0000000..d3614da
--- /dev/null
+++ b/constants/profile-theme.ts
@@ -0,0 +1,15 @@
+/** Profile-specific chrome — soft white on navy, separate from global amber accent. */
+export const PROFILE_ICON = "rgba(255, 255, 255, 0.82)";
+export const PROFILE_ICON_MUTED = "rgba(255, 255, 255, 0.55)";
+export const PROFILE_ICON_RING = "rgba(255, 255, 255, 0.04)";
+export const PROFILE_BACK_BUTTON_RING = "rgba(255, 255, 255, 0.1)";
+export const PROFILE_ICON_SOFT_BG = "rgba(255, 255, 255, 0.08)";
+export const PROFILE_SWITCH_TRACK_ON = "rgba(255, 255, 255, 0.35)";
+export const PROFILE_EDIT_BADGE_BG = "#e2e8f0";
+export const PROFILE_EDIT_BADGE_ICON = "#0b132b";
+/** Secondary line under titles — higher contrast for readability on navy. */
+export const PROFILE_TEXT_SUBTITLE = "rgba(255, 255, 255, 0.72)";
+/** Space below page title before hero identity (avatar). */
+export const PROFILE_HEADER_BOTTOM_GAP = 16;
+/** Space above avatar within the hero identity block. */
+export const PROFILE_HERO_TOP_PADDING = 24;
diff --git a/constants/sign-up-theme.ts b/constants/sign-up-theme.ts
index 724fdba..8565c94 100644
--- a/constants/sign-up-theme.ts
+++ b/constants/sign-up-theme.ts
@@ -21,8 +21,10 @@ export const SIGN_UP_COLORS = {
link: "#F5B84C",
dividerLine: "rgba(255, 255, 255, 0.12)",
socialBg: "rgba(255, 255, 255, 0.06)",
+ socialBgPressed: "rgba(255, 255, 255, 0.16)",
/** Outlined secondary control — visible edge without competing with primary CTA */
socialBorder: "rgba(255, 255, 255, 0.14)",
+ socialBorderPressed: "rgba(255, 255, 255, 0.38)",
socialText: "#FFFFFF",
google: "#EA4335",
backButtonBorder: "rgba(255, 255, 255, 0.1)",
diff --git a/hooks/use-auth-verification-flow.ts b/hooks/use-auth-verification-flow.ts
deleted file mode 100644
index f3281b0..0000000
--- a/hooks/use-auth-verification-flow.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { router } from "expo-router";
-import { useCallback, useState } from "react";
-
-import { useOnboardingStore } from "@/store/use-onboarding-store";
-
-export function useAuthVerificationFlow() {
- const completeOnboarding = useOnboardingStore(
- (state) => state.completeOnboarding,
- );
- const [isVerificationVisible, setIsVerificationVisible] = useState(false);
-
- const openVerification = useCallback(() => {
- setIsVerificationVisible(true);
- }, []);
-
- const closeVerification = useCallback(() => {
- setIsVerificationVisible(false);
- }, []);
-
- const completeVerification = useCallback(() => {
- setIsVerificationVisible(false);
- completeOnboarding();
- router.replace("/");
- }, [completeOnboarding]);
-
- return {
- isVerificationVisible,
- openVerification,
- closeVerification,
- completeVerification,
- };
-}
diff --git a/hooks/use-clerk-auth-flow.ts b/hooks/use-clerk-auth-flow.ts
new file mode 100644
index 0000000..802967f
--- /dev/null
+++ b/hooks/use-clerk-auth-flow.ts
@@ -0,0 +1,261 @@
+import { useAuth, useSignIn, useSignUp, useSSO } from "@clerk/expo";
+import { type Href, useRouter } from "expo-router";
+import { useCallback, useState } from "react";
+
+import { oauthRedirectUrl } from "@/lib/clerk";
+
+export type AuthVerificationMode = "sign-up" | "sign-in-trust" | null;
+
+type FinalizeTarget = {
+ finalize: (options: {
+ navigate: (params: {
+ session?: { currentTask?: { key: string } | null };
+ decorateUrl: (path: string) => string;
+ }) => void;
+ }) => Promise;
+};
+
+export function useClerkAuthFlow(isLogin: boolean) {
+ const router = useRouter();
+ const { isSignedIn } = useAuth();
+ const {
+ signIn,
+ errors: signInErrors,
+ fetchStatus: signInFetchStatus,
+ } = useSignIn();
+ const {
+ signUp,
+ errors: signUpErrors,
+ fetchStatus: signUpFetchStatus,
+ } = useSignUp();
+ const { startSSOFlow } = useSSO();
+
+ const [verificationMode, setVerificationMode] =
+ useState(null);
+ const [verificationEmail, setVerificationEmail] = useState("");
+ const [formError, setFormError] = useState(null);
+
+ const isSubmitting = isLogin
+ ? signInFetchStatus === "fetching"
+ : signUpFetchStatus === "fetching";
+
+ const fieldErrors = isLogin ? signInErrors?.fields : signUpErrors?.fields;
+ const verificationErrors =
+ verificationMode === "sign-up"
+ ? signUpErrors?.fields
+ : signInErrors?.fields;
+
+ const finalizeSession = useCallback(
+ async (target: FinalizeTarget) => {
+ await target.finalize({
+ navigate: ({ session, decorateUrl }) => {
+ if (session?.currentTask) {
+ console.warn("Pending session task:", session.currentTask);
+ return;
+ }
+
+ const url = decorateUrl("/");
+ router.replace(url as Href);
+ },
+ });
+ },
+ [router],
+ );
+
+ const openVerification = useCallback(
+ (mode: Exclude, email: string) => {
+ setFormError(null);
+ setVerificationEmail(email);
+ setVerificationMode(mode);
+ },
+ [],
+ );
+
+ const closeVerification = useCallback(() => {
+ setVerificationMode(null);
+ setVerificationEmail("");
+ setFormError(null);
+ }, []);
+
+ const handleSignIn = useCallback(
+ async (emailAddress: string, password: string) => {
+ setFormError(null);
+
+ const { error } = await signIn.password({
+ emailAddress,
+ password,
+ });
+
+ if (error) {
+ setFormError(error.message ?? "Unable to sign in. Try again.");
+ return;
+ }
+
+ if (signIn.status === "complete") {
+ await finalizeSession(signIn);
+ return;
+ }
+
+ if (signIn.status === "needs_client_trust") {
+ const emailCodeFactor = signIn.supportedSecondFactors?.find(
+ (factor) => factor.strategy === "email_code",
+ );
+
+ if (emailCodeFactor) {
+ const { error: sendError } = await signIn.mfa.sendEmailCode();
+ if (sendError) {
+ setFormError(
+ sendError.message ?? "Unable to send verification code.",
+ );
+ return;
+ }
+
+ openVerification("sign-in-trust", emailAddress);
+ return;
+ }
+ }
+
+ if (signIn.status === "needs_second_factor") {
+ setFormError(
+ "Multi-factor authentication is required for this account.",
+ );
+ return;
+ }
+
+ setFormError("Sign-in could not be completed. Please try again.");
+ },
+ [finalizeSession, openVerification, signIn],
+ );
+
+ const handleSignUp = useCallback(
+ async (emailAddress: string, password: string) => {
+ setFormError(null);
+
+ const { error } = await signUp.password({
+ emailAddress,
+ password,
+ });
+
+ if (error) {
+ setFormError(error.message ?? "Unable to create account. Try again.");
+ return;
+ }
+
+ const { error: sendError } = await signUp.verifications.sendEmailCode();
+ if (sendError) {
+ setFormError(sendError.message ?? "Unable to send verification code.");
+ return;
+ }
+
+ openVerification("sign-up", emailAddress);
+ },
+ [openVerification, signUp],
+ );
+
+ const handleVerifyCode = useCallback(
+ async (code: string) => {
+ setFormError(null);
+
+ if (verificationMode === "sign-up") {
+ const { error } = await signUp.verifications.verifyEmailCode({ code });
+ if (error) {
+ setFormError(error.message ?? "Invalid verification code.");
+ return;
+ }
+
+ if (signUp.status === "complete") {
+ await finalizeSession(signUp);
+ closeVerification();
+ }
+
+ return;
+ }
+
+ if (verificationMode === "sign-in-trust") {
+ const { error } = await signIn.mfa.verifyEmailCode({ code });
+ if (error) {
+ setFormError(error.message ?? "Invalid verification code.");
+ return;
+ }
+
+ if (signIn.status === "complete") {
+ await finalizeSession(signIn);
+ closeVerification();
+ }
+ }
+ },
+ [closeVerification, finalizeSession, signIn, signUp, verificationMode],
+ );
+
+ const handleResendCode = useCallback(async () => {
+ setFormError(null);
+
+ if (verificationMode === "sign-up") {
+ const { error } = await signUp.verifications.sendEmailCode();
+ if (error) {
+ setFormError(error.message ?? "Unable to resend verification code.");
+ }
+ return;
+ }
+
+ if (verificationMode === "sign-in-trust") {
+ const { error } = await signIn.mfa.sendEmailCode();
+ if (error) {
+ setFormError(error.message ?? "Unable to resend verification code.");
+ }
+ }
+ }, [signIn, signUp, verificationMode]);
+
+ const handleSSO = useCallback(
+ async (strategy: "oauth_google" | "oauth_apple") => {
+ setFormError(null);
+
+ try {
+ const { createdSessionId, setActive } = await startSSOFlow({
+ strategy,
+ redirectUrl: oauthRedirectUrl,
+ });
+
+ if (createdSessionId && setActive) {
+ await setActive({ session: createdSessionId });
+ router.replace("/");
+ }
+ } catch (error) {
+ const message =
+ error instanceof Error
+ ? error.message
+ : "Social sign-in failed. Try again.";
+ setFormError(message);
+ }
+ },
+ [router, startSSOFlow],
+ );
+
+ const submitCredentials = useCallback(
+ async (emailAddress: string, password: string) => {
+ if (isLogin) {
+ await handleSignIn(emailAddress, password);
+ return;
+ }
+
+ await handleSignUp(emailAddress, password);
+ },
+ [handleSignIn, handleSignUp, isLogin],
+ );
+
+ return {
+ isSignedIn,
+ isSubmitting,
+ formError,
+ fieldErrors,
+ verificationErrors,
+ verificationMode,
+ verificationEmail,
+ isVerificationVisible: verificationMode !== null,
+ submitCredentials,
+ handleVerifyCode,
+ handleResendCode,
+ handleSSO,
+ closeVerification,
+ };
+}
diff --git a/hooks/use-header-back-button.ts b/hooks/use-header-back-button.ts
new file mode 100644
index 0000000..67ea918
--- /dev/null
+++ b/hooks/use-header-back-button.ts
@@ -0,0 +1,29 @@
+import { router, useNavigation } from "expo-router";
+import * as Haptics from "expo-haptics";
+import { useNavigationState } from "@react-navigation/native";
+import { useCallback } from "react";
+
+/**
+ * True when this screen was pushed onto a stack or opened from another navigator
+ * (e.g. profile tab root reached via router.push, not tab switch).
+ */
+export function useHeaderBackButton() {
+ const navigation = useNavigation();
+ const stackIndex = useNavigationState((state) => state.index) ?? 0;
+
+ const parentCanGoBack = navigation.getParent()?.canGoBack() ?? false;
+ const visible = stackIndex > 0 || parentCanGoBack;
+
+ const onBackPress = useCallback(() => {
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (navigation.canGoBack()) {
+ navigation.goBack();
+ return;
+ }
+ if (router.canGoBack()) {
+ router.back();
+ }
+ }, [navigation]);
+
+ return { visible, onBackPress };
+}
diff --git a/hooks/use-profile-stats.ts b/hooks/use-profile-stats.ts
new file mode 100644
index 0000000..497ea34
--- /dev/null
+++ b/hooks/use-profile-stats.ts
@@ -0,0 +1,104 @@
+import { useMemo } from "react";
+
+import { useCountryFeedStore } from "@/store/use-country-feed-store";
+import { useDiscoveryProgressStore } from "@/store/use-discovery-progress-store";
+import { useSavedCountriesStore } from "@/store/use-saved-countries-store";
+import type { Country } from "@/types/country";
+
+const DEFAULT_CITIES = 56;
+const DEFAULT_PLACES = 128;
+const DEFAULT_PHOTOS = 128;
+
+export type ProfileInlineStats = {
+ countries: number;
+ cities: number;
+ places: number;
+};
+
+export type VisitedCountryPreview = {
+ id: string;
+ cca2: string;
+ flag: string;
+ visitedAt: number;
+};
+
+export function useProfileStats() {
+ const countriesExplored = useDiscoveryProgressStore(
+ (s) => s.countriesExplored,
+ );
+ const visitedCountryIds = useDiscoveryProgressStore(
+ (s) => s.visitedCountryIds,
+ );
+ const visitedAtByCountryId = useDiscoveryProgressStore(
+ (s) => s.visitedAtByCountryId,
+ );
+ const savedCountries = useSavedCountriesStore((s) => s.savedCountries);
+ const feedCountries = useCountryFeedStore((s) => s.countries);
+
+ const allKnownCountries = useMemo(() => {
+ const byId = new Map();
+ for (const country of [...feedCountries, ...savedCountries]) {
+ const id = country.cca2?.trim().toUpperCase() || country.name.trim();
+ if (!byId.has(id)) {
+ byId.set(id, country);
+ }
+ }
+ return byId;
+ }, [feedCountries, savedCountries]);
+
+ const citiesCount =
+ countriesExplored > 0
+ ? Math.round(countriesExplored * 2.3)
+ : DEFAULT_CITIES;
+
+ const placesCount =
+ savedCountries.length > 0 ? savedCountries.length : DEFAULT_PLACES;
+
+ const photosCount =
+ savedCountries.length > 0 ? savedCountries.length : DEFAULT_PHOTOS;
+
+ const continentsCount = useMemo(() => {
+ const regions = new Set();
+ for (const id of visitedCountryIds) {
+ const country = allKnownCountries.get(id);
+ if (country?.region) {
+ regions.add(country.region);
+ }
+ }
+ if (regions.size > 0) return regions.size;
+ return countriesExplored > 0 ? 1 : 7;
+ }, [allKnownCountries, countriesExplored, visitedCountryIds]);
+
+ const topVisitedCountries = useMemo((): VisitedCountryPreview[] => {
+ return visitedCountryIds
+ .map((id) => {
+ const country = allKnownCountries.get(id);
+ return {
+ id,
+ cca2: country?.cca2 ?? id,
+ flag: country?.flag ?? "",
+ visitedAt: visitedAtByCountryId[id] ?? 0,
+ };
+ })
+ .sort((a, b) => b.visitedAt - a.visitedAt);
+ }, [allKnownCountries, visitedAtByCountryId, visitedCountryIds]);
+
+ const savedThumbnailUri =
+ savedCountries[0]?.images?.[0] ?? savedCountries[0]?.flag ?? null;
+
+ return {
+ inlineStats: {
+ countries: countriesExplored,
+ cities: citiesCount,
+ places: placesCount,
+ } satisfies ProfileInlineStats,
+ countriesExplored,
+ citiesCount,
+ placesCount,
+ photosCount,
+ continentsCount,
+ savedCount: savedCountries.length,
+ topVisitedCountries,
+ savedThumbnailUri,
+ };
+}
diff --git a/lib/clerk.ts b/lib/clerk.ts
new file mode 100644
index 0000000..2c1330f
--- /dev/null
+++ b/lib/clerk.ts
@@ -0,0 +1,8 @@
+export const clerkPublishableKey =
+ process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? "";
+
+if (!clerkPublishableKey) {
+ throw new Error("Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file");
+}
+
+export const oauthRedirectUrl = "worldloop://oauth-callback";
diff --git a/package-lock.json b/package-lock.json
index 0ae0465..b73d5f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "worldloop",
"version": "1.0.0",
"dependencies": {
+ "@clerk/expo": "^3.4.1",
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/slider": "5.0.1",
@@ -18,6 +19,7 @@
"earcut": "^3.0.2",
"expo": "~54.0.35",
"expo-asset": "~12.0.13",
+ "expo-auth-session": "~7.0.11",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.12",
@@ -27,6 +29,7 @@
"expo-linear-gradient": "~15.0.8",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.24",
+ "expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
@@ -75,6 +78,12 @@
}
}
},
+ "node_modules/@adraffy/ens-normalize": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+ "license": "MIT"
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1586,6 +1595,248 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@base-org/account": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.0.1.tgz",
+ "integrity": "sha512-tySVNx+vd6XEynZL0uvB10uKiwnAfThr8AbKTwILVG86mPbLAhEOInQIk+uDnvpTvfdUhC1Bi5T/46JvFoLZQQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/hashes": "1.4.0",
+ "clsx": "1.2.1",
+ "eventemitter3": "5.0.1",
+ "idb-keyval": "6.2.1",
+ "ox": "0.6.9",
+ "preact": "10.24.2",
+ "viem": "^2.31.7",
+ "zustand": "5.0.3"
+ }
+ },
+ "node_modules/@base-org/account/node_modules/zustand": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+ "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@clerk/clerk-js": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/@clerk/clerk-js/-/clerk-js-6.16.0.tgz",
+ "integrity": "sha512-8xv/XDsxhOZd1n4DNIRJ2EehIRUg6UiqKAnfd0L88R2t1g6sVnLi1FInJ5i8Qyx5oY/creXx6X1AZ1V5PobRkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@base-org/account": "2.0.1",
+ "@clerk/shared": "^4.17.0",
+ "@coinbase/wallet-sdk": "4.3.7",
+ "@solana/wallet-adapter-base": "0.9.27",
+ "@solana/wallet-adapter-react": "0.15.39",
+ "@solana/wallet-standard": "1.1.4",
+ "@stripe/stripe-js": "5.6.0",
+ "@swc/helpers": "0.5.21",
+ "@tanstack/query-core": "^5.100.6",
+ "@wallet-standard/core": "1.1.1",
+ "@zxcvbn-ts/core": "3.0.4",
+ "@zxcvbn-ts/language-common": "3.0.4",
+ "alien-signals": "2.0.6",
+ "browser-tabs-lock": "1.3.0",
+ "core-js": "3.47.0",
+ "crypto-js": "^4.2.0",
+ "dequal": "2.0.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+ },
+ "node_modules/@clerk/clerk-js/node_modules/@clerk/shared": {
+ "version": "4.17.0",
+ "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.17.0.tgz",
+ "integrity": "sha512-YeQ+6zDmqyor1mPHjZx18j+LssL6Pobvid8hb7HQMioSo8sGDBEVi/Z12bs+gUhe9KbdP+ygHsKOqqeGAPuPZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "^5.100.6",
+ "dequal": "2.0.3",
+ "glob-to-regexp": "0.4.1",
+ "js-cookie": "3.0.7"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@clerk/clerk-js/node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@clerk/clerk-js/node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/@clerk/expo": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@clerk/expo/-/expo-3.4.1.tgz",
+ "integrity": "sha512-gpAXsuUnsUdUD0/2XjyxaC9quF5rT+2umkmV74nBLVAFurGMMMMHvnHqrQEtZ7tH5GHNXYw5+pgmnzd1HiMQbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@clerk/clerk-js": "^6.16.0",
+ "@clerk/react": "^6.9.0",
+ "@clerk/shared": "^4.17.0",
+ "base-64": "^1.0.0",
+ "react-native-url-polyfill": "2.0.0",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "@clerk/expo-passkeys": ">=0.0.6",
+ "expo": ">=53 <57",
+ "expo-apple-authentication": ">=7.0.0",
+ "expo-auth-session": ">=5",
+ "expo-constants": ">=12",
+ "expo-crypto": ">=12",
+ "expo-local-authentication": ">=13.5.0",
+ "expo-secure-store": ">=12.4.0",
+ "expo-web-browser": ">=12.5.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "react-native": ">=0.73"
+ },
+ "peerDependenciesMeta": {
+ "@clerk/expo-passkeys": {
+ "optional": true
+ },
+ "expo-apple-authentication": {
+ "optional": true
+ },
+ "expo-auth-session": {
+ "optional": true
+ },
+ "expo-constants": {
+ "optional": true
+ },
+ "expo-crypto": {
+ "optional": true
+ },
+ "expo-local-authentication": {
+ "optional": true
+ },
+ "expo-secure-store": {
+ "optional": true
+ },
+ "expo-web-browser": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@clerk/expo/node_modules/@clerk/react": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.9.0.tgz",
+ "integrity": "sha512-M0QGyGS732tYBXeG+28UgElXM2TfoSZ+4mWGisC8yxJX8NjH4hEPJTAQuZmYRLNaCyGQCuzjYVQiQRC+GbDtmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@clerk/shared": "^4.17.0",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ }
+ },
+ "node_modules/@clerk/expo/node_modules/@clerk/shared": {
+ "version": "4.17.0",
+ "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.17.0.tgz",
+ "integrity": "sha512-YeQ+6zDmqyor1mPHjZx18j+LssL6Pobvid8hb7HQMioSo8sGDBEVi/Z12bs+gUhe9KbdP+ygHsKOqqeGAPuPZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "^5.100.6",
+ "dequal": "2.0.3",
+ "glob-to-regexp": "0.4.1",
+ "js-cookie": "3.0.7"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@coinbase/wallet-sdk": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.7.tgz",
+ "integrity": "sha512-z6e5XDw6EF06RqkeyEa+qD0dZ2ZbLci99vx3zwDY//XO8X7166tqKJrR2XlQnzVmtcUuJtCd5fCvr9Cu6zzX7w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/hashes": "^1.4.0",
+ "clsx": "^1.2.1",
+ "eventemitter3": "^5.0.1",
+ "preact": "^10.24.2",
+ "viem": "^2.27.2"
+ }
+ },
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -3252,6 +3503,57 @@
"@emnapi/runtime": "^1.7.1"
}
},
+ "node_modules/@noble/ciphers": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/curves": {
+ "version": "1.9.7",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
+ "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.8.0"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/curves/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nolyfill/is-core-module": {
"version": "1.0.39",
"resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
@@ -4516,6 +4818,66 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@scure/base": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+ "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+ "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "~1.9.0",
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip39": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+ "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip39/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
@@ -4540,67 +4902,3313 @@
"@sinonjs/commons": "^3.0.0"
}
},
- "node_modules/@tailwindcss/node": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
- "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
- "dev": true,
- "license": "MIT",
+ "node_modules/@solana-mobile/mobile-wallet-adapter-protocol": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@solana-mobile/mobile-wallet-adapter-protocol/-/mobile-wallet-adapter-protocol-2.2.9.tgz",
+ "integrity": "sha512-qtWJq0hzKgngVG0So2ViBRPYDculaxgj40WDSP1nzgdg85MXyAmTEjQqV10uSVbZRm9b1hl601HDJX1sHTgeiw==",
+ "license": "Apache-2.0",
"dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "^5.21.0",
- "jiti": "^2.6.1",
- "lightningcss": "1.32.0",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.3.0"
+ "@solana/kit": "^6.0.0",
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@solana/wallet-standard-util": "^1.1.2",
+ "@wallet-standard/core": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react-native": ">0.74"
}
},
- "node_modules/@tailwindcss/oxide": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
- "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20"
+ "node_modules/@solana-mobile/mobile-wallet-adapter-protocol-web3js": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@solana-mobile/mobile-wallet-adapter-protocol-web3js/-/mobile-wallet-adapter-protocol-web3js-2.2.9.tgz",
+ "integrity": "sha512-TAMItAuOb7duwYQ+PZ6JNqw/6TEhCWLObHF5uOBGV9tjCozHGVunxa7lTeuHAWIgO68IAo86MZdE5kethDATpw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9"
+ },
+ "peerDependencies": {
+ "@solana/web3.js": "^1.98.4"
+ }
+ },
+ "node_modules/@solana-mobile/wallet-standard-mobile": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@solana-mobile/wallet-standard-mobile/-/wallet-standard-mobile-0.5.3.tgz",
+ "integrity": "sha512-/Ea/CNIWHExpXsA6syVy7fUUhONtYob+VMsmF8flm8GEAZQyzX6UtKSy4NQMMTGBTAGlmmmbBVrZF+Tp5/fDxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9",
+ "@solana/wallet-standard-chains": "^1.1.1",
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@wallet-standard/base": "^1.0.1",
+ "@wallet-standard/features": "^1.0.3",
+ "@wallet-standard/wallet": "^1.1.0",
+ "qrcode": "^1.5.4",
+ "tslib": "^2.8.1"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.3.0",
- "@tailwindcss/oxide-darwin-arm64": "4.3.0",
- "@tailwindcss/oxide-darwin-x64": "4.3.0",
- "@tailwindcss/oxide-freebsd-x64": "4.3.0",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
- "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
- "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
- "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
- "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
- "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
+ "@react-native-async-storage/async-storage": "^1.17.7"
}
},
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
- "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@solana-mobile/wallet-standard-mobile/node_modules/@react-native-async-storage/async-storage": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
+ "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
"license": "MIT",
"optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 20"
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
- "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
+ "node_modules/@solana/accounts": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-6.9.0.tgz",
+ "integrity": "sha512-g36AJreJrgf9AAjOfbdFHEFUTymBgzbWHoEDElZ+fDKvqBINDiUVKzDApwc7C7kGPMFqQBaoEHnQRxf2IqfKZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/rpc-spec": "6.9.0",
+ "@solana/rpc-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/accounts/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/accounts/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/accounts/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/accounts/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/addresses": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-6.9.0.tgz",
+ "integrity": "sha512-tWnG2L6lo/ZhcMT019F3myDsH87MM8EZbTO0cgwgvVPlEdIGblROFF3tGVrb7FVCOlbPI0ONCFyPbnrmR58LsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/assertions": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/nominal-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/addresses/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/addresses/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/addresses/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/addresses/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/assertions": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-6.9.0.tgz",
+ "integrity": "sha512-FjWWD6e0in+HFsHMvU2zKCbyPfKtDW6iGXZZ9+Qg1QUYpO1AEObsya3F7hb9RkZKUueK4WwWAQnIuvEUp3A1uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/assertions/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/assertions/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/assertions/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/buffer-layout": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
+ "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer": "~6.0.3"
+ },
+ "engines": {
+ "node": ">=5.10"
+ }
+ },
+ "node_modules/@solana/codecs": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-6.9.0.tgz",
+ "integrity": "sha512-oWOybKa1PTGI1D/FyrvGKralADM1jmVZC2AtgEo+4JTKG0+i1p9ZbwNY2UcJqdYsDMDaGHAx0LMAid9LDCxXTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/fixed-points": "6.9.0",
+ "@solana/options": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-core": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
+ "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@solana/errors": "2.3.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/codecs-data-structures": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-6.9.0.tgz",
+ "integrity": "sha512-f7GYtiHafvJDhqiwzUUSr/6AYSK4DCw6quPmA80NZGtkNiFa+g6LoJy2wbC0wp2dxvCwNpxf6x3ILCYRutAvvg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-data-structures/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-data-structures/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-data-structures/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-data-structures/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/codecs-data-structures/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/codecs-numbers": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
+ "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@solana/codecs-core": "2.3.0",
+ "@solana/errors": "2.3.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/codecs-strings": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-6.9.0.tgz",
+ "integrity": "sha512-PTqYQxMsmdfEEq29bV1AnALD4FjFEsSxOj1fYNqooOSTEQEpUoYEQtsd55/kBsnIKltXbvYwXYXBusm19n1sQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "fastestsmallesttextencoderdecoder": "^1.0.22",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "fastestsmallesttextencoderdecoder": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-strings/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-strings/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-strings/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs-strings/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/codecs-strings/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/codecs/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/codecs/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/codecs/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/errors": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
+ "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "chalk": "^5.4.1",
+ "commander": "^14.0.0"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/errors/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/errors/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/fast-stable-stringify": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-6.9.0.tgz",
+ "integrity": "sha512-l14zGVsURbT5Aox/kLFQywqV4VaE9/j3h2EvCu9oULVPMwzQB6yezJb1/KyiDwhm/RscooPd0gFQFIKEGQbayw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/fixed-points": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/fixed-points/-/fixed-points-6.9.0.tgz",
+ "integrity": "sha512-0K7mbYC4jdAZFlXqXjpNanmEyZxk7K9NtXDLc1zuhGuxwH8J9guvohwdw2V7TQ9bfjCYsprY3Tp2kUVQpECGmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/fixed-points/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/fixed-points/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/fixed-points/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/fixed-points/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/functional": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-6.9.0.tgz",
+ "integrity": "sha512-sgNHOaIjETZZuziZdlwPsU5EjBVj5M0dUbwrSQTTNZe0SxX3pQ1QFVcs5KyvdS7AQcpBVdLjx4CfQjdKXk52GA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instruction-plans": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/instruction-plans/-/instruction-plans-6.9.0.tgz",
+ "integrity": "sha512-SxTSOetEKD+WPzvDuYRsP1+KkwUp8KqL1n7oFx9ThxjyfEY0ly0i9KdbvX5yYVDOA2TSwrltgdu14y/Pf6y3Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/promises": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instruction-plans/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instruction-plans/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/instruction-plans/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/instructions": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-6.9.0.tgz",
+ "integrity": "sha512-LZfJx3bGdUSbGaswoOEPHygticqkCg3TusRczPJXyCmKhoQzPCcGQQ99qMzP7Wg8pEV5tWA5t7tycf8E237ydg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instructions/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instructions/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/instructions/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/instructions/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/keys": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-6.9.0.tgz",
+ "integrity": "sha512-1g2QARiqSjNqT0EIqLDLQ5vRm7hCsbqgFwFAp5GsMV/8BTYT8s1Ct2wLHDZiJ4eAX6beTHVf8LbOBfVejtn3oQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/assertions": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/nominal-types": "6.9.0",
+ "@solana/promises": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/keys/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/keys/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/keys/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/keys/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/kit": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-6.9.0.tgz",
+ "integrity": "sha512-k7BRz7Akfv8wiRtlCR/xUyDLfuMfYMelMR1+AC5KgwaRRJReDF0BucMLNN1In7WoI+KuWwr1OKv4na/oKpyeAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/accounts": "6.9.0",
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/instruction-plans": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/offchain-messages": "6.9.0",
+ "@solana/plugin-core": "6.9.0",
+ "@solana/plugin-interfaces": "6.9.0",
+ "@solana/program-client-core": "6.9.0",
+ "@solana/programs": "6.9.0",
+ "@solana/rpc": "6.9.0",
+ "@solana/rpc-api": "6.9.0",
+ "@solana/rpc-parsed-types": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "@solana/rpc-subscriptions": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/signers": "6.9.0",
+ "@solana/subscribable": "6.9.0",
+ "@solana/sysvars": "6.9.0",
+ "@solana/transaction-confirmation": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/kit/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/kit/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/kit/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/nominal-types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-6.9.0.tgz",
+ "integrity": "sha512-ouhrnY7a6nsLXRGcariwcmHDdXroCNqOuzwtdjKt2c8e8Drwao9yxPH2VoViNgpq8IGNJeQMEI1TVnoJZRn0gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/offchain-messages": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/offchain-messages/-/offchain-messages-6.9.0.tgz",
+ "integrity": "sha512-qK3tqRPb+E0kmTz5qFXZbEdF4pyzfOWRZjyVESHVGemDDeGzZ1SV3zAxcA6HBCnv4wCBnlyaDPw8t+5sryNMAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/nominal-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/offchain-messages/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/offchain-messages/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/offchain-messages/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/offchain-messages/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/offchain-messages/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/options": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/options/-/options-6.9.0.tgz",
+ "integrity": "sha512-H5ZRWNzzLMwHU/fRU9aVx+3TaMN4gDNCUYxsZxq0h7mqiwxFy6mpy95xPsfdldthCHDYtYnUTxe2sBatGbNHig==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/options/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/options/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/options/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/options/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/options/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/plugin-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-6.9.0.tgz",
+ "integrity": "sha512-KslLSnzY8zbGZibEBVMVUm2ZS8T2xf+cut7F65VjWPoWNAxU+p7933wsMz/az6CF7b65RI7iU3HhCr5/5QF50w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/plugin-interfaces": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/plugin-interfaces/-/plugin-interfaces-6.9.0.tgz",
+ "integrity": "sha512-Qj4sk9thkM1UgnFXvWIoezd/CbqpX/2jigLBDsMB5Ed/gmFlkBSTL127LFDSY3OtzBpXl4hROs+Zqv+5xqtguA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/instruction-plans": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/rpc-spec": "6.9.0",
+ "@solana/rpc-subscriptions-spec": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/signers": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/program-client-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/program-client-core/-/program-client-core-6.9.0.tgz",
+ "integrity": "sha512-+iUnsddhs72QoBJoUO+/yHUXoBvYWa1sGCBRJk35zeg8j7ZXEwRkk6eX0VOrUPxhEpQbYJsIOCrIYApNIt8RFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/accounts": "6.9.0",
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/instruction-plans": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/plugin-interfaces": "6.9.0",
+ "@solana/rpc-api": "6.9.0",
+ "@solana/signers": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/program-client-core/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/program-client-core/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/program-client-core/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/program-client-core/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/programs": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-6.9.0.tgz",
+ "integrity": "sha512-L9LAnQtfFFcCDLcbbnxhUtgAmu/kS4aRmrVncdnX5CFyQshlpo0/Qhrq3UA7vnhute4gjYV4pFT+64onH5qGEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/programs/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/programs/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/programs/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/promises": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-6.9.0.tgz",
+ "integrity": "sha512-227PlXRi6KZX4ODYTkJitr9InSa79NTquI72slay4gzxO9VmMepgvYdMAX6kawdN5pt+VzaklKhNhWXk50Pi9g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-6.9.0.tgz",
+ "integrity": "sha512-ny1Kt20+oq3xZErNA56+Magmb2JKYfQgHwZTsBmHKVl/9mBpv1y1+ygV+KNiiX/wWXWstLbdIo1jgPwZPbU2Vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/fast-stable-stringify": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/rpc-api": "6.9.0",
+ "@solana/rpc-spec": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "@solana/rpc-transformers": "6.9.0",
+ "@solana/rpc-transport-http": "6.9.0",
+ "@solana/rpc-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-api": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-6.9.0.tgz",
+ "integrity": "sha512-3KhXS6A1ie6GqTywW/KEMSXJ1VJEU66fxjhuiiqPILuJstP7kex3ycr3H6DirKydUsy6gaKaPN43rE+LfyS7OA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/rpc-parsed-types": "6.9.0",
+ "@solana/rpc-spec": "6.9.0",
+ "@solana/rpc-transformers": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-api/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-api/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-api/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-api/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-parsed-types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-6.9.0.tgz",
+ "integrity": "sha512-6ThH8izY+DWDyrVOOlS40vTcFjwjCinjfqnId7zhRk8OxhkfHQ/iEj+OnGwD4Yhe8pGdVa7GNVYlrQgQgzQ3eQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-spec": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-6.9.0.tgz",
+ "integrity": "sha512-3yHRoChc0IpsJbUq0/94l+ar3t9U3Ax58W0HON7eyYe7zFP10UAxpkHn7DPch9DeALyuGph8kVnvl+kXRgJlGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-spec-types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-6.9.0.tgz",
+ "integrity": "sha512-A4fY1JRrcKqX3EfttO4Q8L97nGPqdjfekAV0eDyxN5nu9ngf5p7GKenkl7AYDoHLNr6ZX/C96cRADxXjsRJ0iA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-spec/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-spec/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-spec/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-6.9.0.tgz",
+ "integrity": "sha512-IMctZQaMxzvRACQ6ooW98lP+7tVoUJnRgOZtkAdzgBizldQAYPIKd3MulP0jbQPCMfdPsa2Hs0NBcUwfgonq3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/fast-stable-stringify": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/promises": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "@solana/rpc-subscriptions-api": "6.9.0",
+ "@solana/rpc-subscriptions-channel-websocket": "6.9.0",
+ "@solana/rpc-subscriptions-spec": "6.9.0",
+ "@solana/rpc-transformers": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/subscribable": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-api": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-6.9.0.tgz",
+ "integrity": "sha512-UA/rPQeNx6zQMUFcS8PPPuB4vzUOtSzIY/igMH0DRoP020NyES2GguIb7Zo7sqDNi4n0gkQRhoW4dPVotcNKdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/rpc-subscriptions-spec": "6.9.0",
+ "@solana/rpc-transformers": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-channel-websocket": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-6.9.0.tgz",
+ "integrity": "sha512-kT8Yne9HjJD2gooaOFNSyKrvaIfOy2GR0Ymv8OfecBCwFStdz+SPo5eYXq8ZWoZbr5E/MMpHgqsHBanqa2Ffyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/rpc-subscriptions-spec": "6.9.0",
+ "@solana/subscribable": "6.9.0",
+ "ws": "^8.19.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-channel-websocket/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-channel-websocket/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-channel-websocket/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-channel-websocket/node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-spec": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-6.9.0.tgz",
+ "integrity": "sha512-DbaG67s99vRZQxFMK80UQ7DEKkRJK6JEZeYg/U5UttD6n7ax/vct7qopxGnrt4RCkaaac2fU8Sr+fcnvWQweUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/promises": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "@solana/subscribable": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-spec/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-spec/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions-spec/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-subscriptions/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-transformers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-6.9.0.tgz",
+ "integrity": "sha512-dg4LK2wEBpaY+KRk/SJIkYvrvjdsc1AwD4bkmGY4Fp7EwVlvwBQShAQn78Qi4IP0WQ/0n9ncFyUxgcB1Y01ZuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/nominal-types": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "@solana/rpc-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-transformers/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-transformers/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-transformers/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-transport-http": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-6.9.0.tgz",
+ "integrity": "sha512-4gy30fWJcS6jrcXCoP/optFpGJ/gD9xdkE8wDbe1Ys/Y+e4XjyBt45xtTnbdmMdukvdRX+oXS3zgUIYoagpNzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0",
+ "@solana/rpc-spec": "6.9.0",
+ "@solana/rpc-spec-types": "6.9.0",
+ "undici-types": "^8.2.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-transport-http/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-transport-http/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-transport-http/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc-transport-http/node_modules/undici-types": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.4.1.tgz",
+ "integrity": "sha512-iIXDNrTeaM0lDZvNUY1Urfs9dVgOWdQCkv6VMiePh644EKce0qoz6FNxxg7/DS4CxbFI36Atlz0VgHKS2qL1Dw==",
+ "license": "MIT"
+ },
+ "node_modules/@solana/rpc-types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-6.9.0.tgz",
+ "integrity": "sha512-iFhPzZK3qiQ1lhfNTNBTI7BIs5PfWZSgRLD3enKm8ZAQggzvUklfO3KPh47jVsc/Jsr1UGPH8M3o3m17qjO1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/fixed-points": "6.9.0",
+ "@solana/nominal-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-types/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-types/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-types/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc-types/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc-types/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/rpc/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/rpc/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/rpc/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/signers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-6.9.0.tgz",
+ "integrity": "sha512-x7WyoRm9IORMqeSqNivZgyY+RERPkmqWxpINPD13kUH+oaZzonORIgxk2Lz+u5iPRXiJPkdRPrQ4FoFWv8i6kQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/nominal-types": "6.9.0",
+ "@solana/offchain-messages": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/signers/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/signers/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/signers/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/signers/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/subscribable": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-6.9.0.tgz",
+ "integrity": "sha512-YV0/BrJNfepf10CTfLwD7kRY1kkELDHd+BbHJZhBeiuiXTY3xQTvvx1RFs3NtfFCcTHG25Uh8NpRacQJnxSSIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/subscribable/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/subscribable/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/subscribable/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/sysvars": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-6.9.0.tgz",
+ "integrity": "sha512-e0e+QKr/th9t/O2N1oUoJmcodLghzAtWKUlGb1zyYub0/WJrPImnKqJqp/gDP4tK98mJxopPMcprCeHk4B+TQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/accounts": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/rpc-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/sysvars/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/sysvars/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/sysvars/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/sysvars/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/sysvars/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/transaction-confirmation": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-6.9.0.tgz",
+ "integrity": "sha512-fzYCOih7hhtBzzNSkAnxMjeFeQ8U7e27k9i0RsgQc3/e3OCynF5HoIVNhhqZbwfIBKiaD4ginJR6slRnfqO32Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/promises": "6.9.0",
+ "@solana/rpc": "6.9.0",
+ "@solana/rpc-subscriptions": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/transaction-messages": "6.9.0",
+ "@solana/transactions": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-confirmation/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-confirmation/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/transaction-confirmation/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/transaction-messages": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-6.9.0.tgz",
+ "integrity": "sha512-OWpryt0w6SHlwHx12Vd1wvx2QwSGBXAIUEHTCtkctcM3AaZRy5cIl7CAq9iD5PgahUsaOyRLBV0zlCJcC2JrJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/nominal-types": "6.9.0",
+ "@solana/rpc-types": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-messages/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-messages/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-messages/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transaction-messages/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/transaction-messages/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/transactions": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-6.9.0.tgz",
+ "integrity": "sha512-uKPzLwHbjwChfVl82he17ntkh02PfgnMMhN7uOAC+VbkIt1O+EEw8sX87gi6kdG/EV+QBDQXm9PLAo5W0tYylw==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/addresses": "6.9.0",
+ "@solana/codecs-core": "6.9.0",
+ "@solana/codecs-data-structures": "6.9.0",
+ "@solana/codecs-numbers": "6.9.0",
+ "@solana/codecs-strings": "6.9.0",
+ "@solana/errors": "6.9.0",
+ "@solana/functional": "6.9.0",
+ "@solana/instructions": "6.9.0",
+ "@solana/keys": "6.9.0",
+ "@solana/nominal-types": "6.9.0",
+ "@solana/rpc-types": "6.9.0",
+ "@solana/transaction-messages": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transactions/node_modules/@solana/codecs-core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-6.9.0.tgz",
+ "integrity": "sha512-F2BmLecG/1nTtnjyD509NsEc254pxJKa2bpvotymv1lL1WfEn3zchcZ9SMIiLyL4G6J8b9F3OKIq2YSZho2AOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transactions/node_modules/@solana/codecs-numbers": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-6.9.0.tgz",
+ "integrity": "sha512-XMI0FOHV2h7yPAllxWCX8z+J1msidNjXzN1mRjH5KR6C+vfzyKa2xWHve0bNSV/bjVAhqqhc7dQCpBKuF4+ScQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "6.9.0",
+ "@solana/errors": "6.9.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transactions/node_modules/@solana/errors": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-6.9.0.tgz",
+ "integrity": "sha512-7i+b07KMnkbHvFlz7uWade3jvyc22UmVm8o9taxPK8YV3JNM/NkS8oQFvMac2MIaLPAlEs7I8MHyVLUal1yY4g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "5.6.2",
+ "commander": "14.0.3"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@solana/transactions/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@solana/transactions/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@solana/wallet-adapter-base": {
+ "version": "0.9.27",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.27.tgz",
+ "integrity": "sha512-kXjeNfNFVs/NE9GPmysBRKQ/nf+foSaq3kfVSeMcO/iVgigyRmB551OjU3WyAolLG/1jeEfKLqF9fKwMCRkUqg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/features": "^1.1.0",
+ "eventemitter3": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@solana/web3.js": "^1.98.0"
+ }
+ },
+ "node_modules/@solana/wallet-adapter-react": {
+ "version": "0.15.39",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-adapter-react/-/wallet-adapter-react-0.15.39.tgz",
+ "integrity": "sha512-WXtlo88ith5m22qB+qiGw301/Zb9r5pYr4QdXWmlXnRNqwST5MGmJWhG+/RVrzc+OG7kSb3z1gkVNv+2X/Y0Gg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana-mobile/wallet-adapter-mobile": "^2.2.0",
+ "@solana/wallet-adapter-base": "^0.9.27",
+ "@solana/wallet-standard-wallet-adapter-react": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@solana/web3.js": "^1.98.0",
+ "react": "*"
+ }
+ },
+ "node_modules/@solana/wallet-adapter-react/node_modules/@solana-mobile/wallet-adapter-mobile": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@solana-mobile/wallet-adapter-mobile/-/wallet-adapter-mobile-2.2.9.tgz",
+ "integrity": "sha512-IWzI2pUmEqBsTNo+XqMTY38NL5TSdMetES59N0Cmuf+zYqiS56rHdMwD/R2IOy7E4yKa1CsuHfCh6CE6zk4ViQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9",
+ "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.2.9",
+ "@solana-mobile/wallet-standard-mobile": "^0.5.3",
+ "@solana/wallet-adapter-base": "^0.9.27",
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@wallet-standard/core": "^1.1.1",
+ "tslib": "^2.8.1"
+ },
+ "optionalDependencies": {
+ "@react-native-async-storage/async-storage": "^1.17.7"
+ },
+ "peerDependencies": {
+ "@solana/web3.js": "^1.98.4",
+ "react-native": ">0.74"
+ }
+ },
+ "node_modules/@solana/wallet-adapter-react/node_modules/@solana-mobile/wallet-adapter-mobile/node_modules/@react-native-async-storage/async-storage": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
+ "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.60 <1.0"
+ }
+ },
+ "node_modules/@solana/wallet-standard": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard/-/wallet-standard-1.1.4.tgz",
+ "integrity": "sha512-NF+MI5tOxyvfTU4A+O5idh/gJFmjm52bMwsPpFGRSL79GECSN0XLmpVOO/jqTKJgac2uIeYDpQw/eMaQuWuUXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-standard-core": "^1.1.2",
+ "@solana/wallet-standard-wallet-adapter": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-chains": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-chains/-/wallet-standard-chains-1.1.1.tgz",
+ "integrity": "sha512-Us3TgL4eMVoVWhuC4UrePlYnpWN+lwteCBlhZDUhFZBJ5UMGh94mYPXno3Ho7+iHPYRtuCi/ePvPcYBqCGuBOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-core": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-core/-/wallet-standard-core-1.1.2.tgz",
+ "integrity": "sha512-FaSmnVsIHkHhYlH8XX0Y4TYS+ebM+scW7ZeDkdXo3GiKge61Z34MfBPinZSUMV08hCtzxxqH2ydeU9+q/KDrLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-standard-chains": "^1.1.1",
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@solana/wallet-standard-util": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-features": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.3.0.tgz",
+ "integrity": "sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/features": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-util": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-util/-/wallet-standard-util-1.1.2.tgz",
+ "integrity": "sha512-rUXFNP4OY81Ddq7qOjQV4Kmkozx4wjYAxljvyrqPx8Ycz0FYChG/hQVWqvgpK3sPsEaO/7ABG1NOACsyAKWNOA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/curves": "^1.8.0",
+ "@solana/wallet-standard-chains": "^1.1.1",
+ "@solana/wallet-standard-features": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-wallet-adapter": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-wallet-adapter/-/wallet-standard-wallet-adapter-1.1.4.tgz",
+ "integrity": "sha512-YSBrxwov4irg2hx9gcmM4VTew3ofNnkqsXQ42JwcS6ykF1P1ecVY8JCbrv75Nwe6UodnqeoZRbN7n/p3awtjNQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-standard-wallet-adapter-base": "^1.1.4",
+ "@solana/wallet-standard-wallet-adapter-react": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@solana/wallet-standard-wallet-adapter-base": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-wallet-adapter-base/-/wallet-standard-wallet-adapter-base-1.1.4.tgz",
+ "integrity": "sha512-Q2Rie9YaidyFA4UxcUIxUsvynW+/gE2noj/Wmk+IOwDwlVrJUAXCvFaCNsPDSyKoiYEKxkSnlG13OA1v08G4iw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-adapter-base": "^0.9.23",
+ "@solana/wallet-standard-chains": "^1.1.1",
+ "@solana/wallet-standard-features": "^1.3.0",
+ "@solana/wallet-standard-util": "^1.1.2",
+ "@wallet-standard/app": "^1.1.0",
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/features": "^1.1.0",
+ "@wallet-standard/wallet": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "@solana/web3.js": "^1.98.0",
+ "bs58": "^6.0.0"
+ }
+ },
+ "node_modules/@solana/wallet-standard-wallet-adapter-react": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-wallet-adapter-react/-/wallet-standard-wallet-adapter-react-1.1.4.tgz",
+ "integrity": "sha512-xa4KVmPgB7bTiWo4U7lg0N6dVUtt2I2WhEnKlIv0jdihNvtyhOjCKMjucWet6KAVhir6I/mSWrJk1U9SvVvhCg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@solana/wallet-standard-wallet-adapter-base": "^1.1.4",
+ "@wallet-standard/app": "^1.1.0",
+ "@wallet-standard/base": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "@solana/wallet-adapter-base": "*",
+ "react": "*"
+ }
+ },
+ "node_modules/@solana/web3.js": {
+ "version": "1.98.4",
+ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
+ "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.25.0",
+ "@noble/curves": "^1.4.2",
+ "@noble/hashes": "^1.4.0",
+ "@solana/buffer-layout": "^4.0.1",
+ "@solana/codecs-numbers": "^2.1.0",
+ "agentkeepalive": "^4.5.0",
+ "bn.js": "^5.2.1",
+ "borsh": "^0.7.0",
+ "bs58": "^4.0.1",
+ "buffer": "6.0.3",
+ "fast-stable-stringify": "^1.0.0",
+ "jayson": "^4.1.1",
+ "node-fetch": "^2.7.0",
+ "rpc-websockets": "^9.0.2",
+ "superstruct": "^2.0.2"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/base-x": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
+ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
+ "node_modules/@stripe/stripe-js": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.6.0.tgz",
+ "integrity": "sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.16"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
+ "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
+ "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.21.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
+ "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-x64": "4.3.0",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.0",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
+ "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
+ "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
"cpu": [
"arm64"
],
@@ -4811,6 +8419,16 @@
"tailwindcss": "4.3.0"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.101.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
+ "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -4874,6 +8492,16 @@
"assertion-error": "^2.0.1"
}
},
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
@@ -5015,12 +8643,29 @@
"meshoptimizer": "~0.18.1"
}
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"license": "MIT"
},
+ "node_modules/@types/ws": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
+ "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -5788,6 +9433,104 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@wallet-standard/app": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/app/-/app-1.1.1.tgz",
+ "integrity": "sha512-WDGwoByhP5gwHH01r5EaLgQdLVkACPCdOMQhmhn8rsm10h/siSgTorShzBxrn0ExSPof+Lu+C3TfgqBrPa1xoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/base": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz",
+ "integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/core": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/core/-/core-1.1.1.tgz",
+ "integrity": "sha512-5Xmjc6+Oe0hcPfVc5n8F77NVLwx1JVAoCVgQpLyv/43/bhtIif+Gx3WUrDlaSDoM8i2kA2xd6YoFbHCxs+e0zA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/app": "^1.1.0",
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/errors": "^0.1.1",
+ "@wallet-standard/features": "^1.1.0",
+ "@wallet-standard/wallet": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@wallet-standard/errors": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/errors/-/errors-0.1.2.tgz",
+ "integrity": "sha512-oEzKUqJefKby6wcIvaJgrSEe/uNn/rnqkJ0P/85K+h0i5Tdo9E3L22VWq/j5K1e8hHMnZd6LgaIr8m/Wn7X/Ng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "chalk": "^5.4.1",
+ "commander": "^13.1.0"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/errors/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@wallet-standard/errors/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@wallet-standard/features": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.1.tgz",
+ "integrity": "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/wallet": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.1.tgz",
+ "integrity": "sha512-8WiRPaKk/wNNRZhB2eVhpR/JW7/aqTCMoZhgVUCujuzDmxxmGvsosMxdCG4NAdYkoyozAHCX8/xLtlWUn5mNdQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
@@ -5797,6 +9540,42 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@zxcvbn-ts/core": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.4.tgz",
+ "integrity": "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==",
+ "license": "MIT",
+ "dependencies": {
+ "fastest-levenshtein": "1.0.16"
+ }
+ },
+ "node_modules/@zxcvbn-ts/language-common": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-common/-/language-common-3.0.4.tgz",
+ "integrity": "sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==",
+ "license": "MIT"
+ },
+ "node_modules/abitype": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz",
+ "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/wevm"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4",
+ "zod": "^3.22.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -5853,6 +9632,19 @@
"node": ">= 14"
}
},
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -5870,6 +9662,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/alien-signals": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.6.tgz",
+ "integrity": "sha512-P3TxJSe31bUHBiblg59oU1PpaWPtmxF9GhJ/cB7OkgJ0qN/ifFSKUI25/v8ZhsT+lIG6ac8DpTOplXxORX6F3Q==",
+ "license": "MIT"
+ },
"node_modules/anser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -6383,6 +10181,19 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
+ "license": "MIT"
+ },
+ "node_modules/base-x": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
+ "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -6453,6 +10264,45 @@
"node": ">=0.6"
}
},
+ "node_modules/bn.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
+ "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/borsh": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
+ "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "bn.js": "^5.2.0",
+ "bs58": "^4.0.0",
+ "text-encoding-utf-8": "^1.0.2"
+ }
+ },
+ "node_modules/borsh/node_modules/base-x": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
+ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/borsh/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -6496,6 +10346,16 @@
"node": ">=8"
}
},
+ "node_modules/browser-tabs-lock": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.3.0.tgz",
+ "integrity": "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": ">=4.17.21"
+ }
+ },
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
@@ -6529,6 +10389,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/bs58": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
+ "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base-x": "^5.0.0"
+ }
+ },
"node_modules/bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -6568,6 +10438,21 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
+ "node_modules/bufferutil": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
+ "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -6822,6 +10707,15 @@
"node": ">=0.8"
}
},
+ "node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -6991,6 +10885,17 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
@@ -7027,6 +10932,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
@@ -7113,6 +11024,15 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
@@ -7214,6 +11134,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delay": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+ "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -7223,6 +11156,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -7248,6 +11190,12 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -7551,6 +11499,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/es6-promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "es6-promise": "^4.0.3"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -8041,6 +12006,12 @@
"node": ">=6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -8103,19 +12074,58 @@
}
}
},
- "node_modules/expo-asset": {
- "version": "12.0.13",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
- "integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==",
+ "node_modules/expo-asset": {
+ "version": "12.0.13",
+ "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
+ "integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.8.8",
+ "expo-constants": "~18.0.13"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-auth-session": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-7.0.11.tgz",
+ "integrity": "sha512-AhWtt/m9rb1Po77X/VBFbeE6UTgbm2vXP2iCblUSRsHCw2qD6lO0ulKUB8Xyxy9FtoI9yrNQ1iwCNgIIgo8VYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-application": "~7.0.8",
+ "expo-constants": "~18.0.13",
+ "expo-crypto": "~15.0.9",
+ "expo-linking": "~8.0.12",
+ "expo-web-browser": "~15.0.11",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-auth-session/node_modules/expo-application": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
+ "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-auth-session/node_modules/expo-crypto": {
+ "version": "15.0.9",
+ "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.9.tgz",
+ "integrity": "sha512-SNWKa2fXx7v9gkp1h/7nqXY5XN7qgNDn3yRc2aO0gWGbeMbvob/haMxxsPFe9f51aqH5NjNCqHf2kvLhvAd8KQ==",
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.8.8",
- "expo-constants": "~18.0.13"
+ "base64-js": "^1.3.0"
},
"peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
+ "expo": "*"
}
},
"node_modules/expo-blur": {
@@ -8622,6 +12632,15 @@
"node": ">=10"
}
},
+ "node_modules/expo-secure-store": {
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
+ "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-server": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.7.tgz",
@@ -9552,6 +13571,15 @@
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0"
},
+ "node_modules/eyes": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
+ "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
+ "peer": true,
+ "engines": {
+ "node": "> 0.1.90"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -9571,6 +13599,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-stable-stringify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
+ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -10000,6 +14044,12 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -10269,12 +14319,28 @@
"node": ">= 14"
}
},
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
+ "node_modules/idb-keyval": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
+ "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
+ "license": "Apache-2.0"
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -10888,6 +14954,31 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
+ "node_modules/isomorphic-ws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
+ "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
+ "node_modules/isows": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+ "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -10943,6 +15034,58 @@
"react": "^19.0.0"
}
},
+ "node_modules/jayson": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
+ "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/connect": "^3.4.33",
+ "@types/node": "^12.12.54",
+ "@types/ws": "^7.4.4",
+ "commander": "^2.20.3",
+ "delay": "^5.0.0",
+ "es6-promisify": "^5.0.0",
+ "eyes": "^0.1.8",
+ "isomorphic-ws": "^4.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "stream-json": "^1.9.1",
+ "uuid": "^8.3.2",
+ "ws": "^7.5.10"
+ },
+ "bin": {
+ "jayson": "bin/jayson.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jayson/node_modules/@types/node": {
+ "version": "12.20.55",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
+ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/jayson/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/jayson/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/jest-environment-node": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
@@ -11144,6 +15287,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz",
+ "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -11201,6 +15353,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "license": "ISC",
+ "peer": true
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -11555,6 +15714,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -12385,6 +16550,19 @@
"node": ">= 6.13.0"
}
},
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
@@ -12796,6 +16974,47 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ox": {
+ "version": "0.6.9",
+ "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz",
+ "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@adraffy/ens-normalize": "^1.10.1",
+ "@noble/curves": "^1.6.0",
+ "@noble/hashes": "^1.5.0",
+ "@scure/bip32": "^1.5.0",
+ "@scure/bip39": "^1.4.0",
+ "abitype": "^1.0.6",
+ "eventemitter3": "5.0.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ox/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -13048,6 +17267,16 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
+ "node_modules/preact": {
+ "version": "10.24.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz",
+ "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -13108,76 +17337,229 @@
"integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==",
"license": "ISC",
"engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
+ "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "asap": "~2.0.6"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode-terminal": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz",
+ "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==",
+ "bin": {
+ "qrcode-terminal": "bin/qrcode-terminal.js"
+ }
+ },
+ "node_modules/qrcode/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "node_modules/qrcode/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
"engines": {
- "node": ">=0.4.0"
+ "node": ">=8"
}
},
- "node_modules/promise": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
- "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
+ "node_modules/qrcode/node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
- "dependencies": {
- "asap": "~2.0.6"
+ "engines": {
+ "node": ">=10.13.0"
}
},
- "node_modules/prompts": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
- "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "node_modules/qrcode/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
- "kleur": "^3.0.3",
- "sisteransi": "^1.0.5"
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
},
"engines": {
- "node": ">= 6"
+ "node": ">=8"
}
},
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
+ "node_modules/qrcode/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "license": "MIT",
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
"engines": {
"node": ">=6"
}
},
- "node_modules/qrcode-terminal": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz",
- "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==",
- "bin": {
- "qrcode-terminal": "bin/qrcode-terminal.js"
- }
- },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
@@ -13479,6 +17861,18 @@
"react-native": "*"
}
},
+ "node_modules/react-native-url-polyfill": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
+ "integrity": "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url-without-unicode": "8.0.0-3"
+ },
+ "peerDependencies": {
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-web": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -13906,6 +18300,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
"node_modules/requireg": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz",
@@ -14088,6 +18488,91 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/rpc-websockets": {
+ "version": "9.3.9",
+ "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz",
+ "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==",
+ "license": "LGPL-3.0-only",
+ "peer": true,
+ "dependencies": {
+ "@swc/helpers": "^0.5.11",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.2.2",
+ "buffer": "^6.0.3",
+ "eventemitter3": "^5.0.1",
+ "uuid": "^14.0.0",
+ "ws": "^8.5.0"
+ },
+ "funding": {
+ "type": "paypal",
+ "url": "https://paypal.me/kozjak"
+ },
+ "optionalDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^6.0.0"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/utf-8-validate": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
+ "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/uuid": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/safe-array-concat": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
@@ -14295,6 +18780,12 @@
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -14681,6 +19172,23 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/stream-chain": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
+ "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/stream-json": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
+ "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "stream-chain": "^2.2.5"
+ }
+ },
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -14900,6 +19408,16 @@
"node": ">= 6"
}
},
+ "node_modules/superstruct": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
+ "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -15081,6 +19599,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/text-encoding-utf-8": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
+ "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==",
+ "peer": true
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -15371,7 +19895,6 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -15629,6 +20152,21 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/utf-8-validate": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
+ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -15856,6 +20394,135 @@
}
}
},
+ "node_modules/viem": {
+ "version": "2.52.2",
+ "resolved": "https://registry.npmjs.org/viem/-/viem-2.52.2.tgz",
+ "integrity": "sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "1.9.1",
+ "@noble/hashes": "1.8.0",
+ "@scure/bip32": "1.7.0",
+ "@scure/bip39": "1.6.0",
+ "abitype": "1.2.3",
+ "isows": "1.0.7",
+ "ox": "0.14.29",
+ "ws": "8.20.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/viem/node_modules/@noble/curves": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
+ "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.8.0"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/viem/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/viem/node_modules/abitype": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
+ "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/wevm"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4",
+ "zod": "^3.22.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/viem/node_modules/ox": {
+ "version": "0.14.29",
+ "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.29.tgz",
+ "integrity": "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@adraffy/ens-normalize": "^1.11.0",
+ "@noble/ciphers": "^1.3.0",
+ "@noble/curves": "1.9.1",
+ "@noble/hashes": "^1.8.0",
+ "@scure/bip32": "^1.7.0",
+ "@scure/bip39": "^1.6.0",
+ "abitype": "^1.2.3",
+ "eventemitter3": "5.0.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/viem/node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
@@ -16208,6 +20875,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
"node_modules/which-typed-array": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz",
diff --git a/package.json b/package.json
index 6971fbd..359a12b 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@clerk/expo": "^3.4.1",
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/slider": "5.0.1",
@@ -23,6 +24,7 @@
"earcut": "^3.0.2",
"expo": "~54.0.35",
"expo-asset": "~12.0.13",
+ "expo-auth-session": "~7.0.11",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.12",
@@ -32,6 +34,7 @@
"expo-linear-gradient": "~15.0.8",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.24",
+ "expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
diff --git a/prompts/05-clerk.md b/prompts/05-clerk.md
index 027dc2c..a73e912 100644
--- a/prompts/05-clerk.md
+++ b/prompts/05-clerk.md
@@ -24,6 +24,8 @@ Do not change the screen design. If there is any need, ask me before implementat
There are three approaches for adding authentication to your Expo app.
+**For AI agents:** In an existing Expo app, run `npx clerk@latest init --framework expo` to install `@clerk/expo` and pull your keys into your project's env file (it won't scaffold screens or bootstrap a new app, so follow the steps below for the provider and UI). Install [Clerk's skills](https://clerk.com/docs/guides/ai/skills.md) with `npx skills add clerk/skills` — includes `clerk-expo-patterns`.
+
| Approach | Auth UI | OAuth | Requires dev build | Best for |
| ----------------------- | ----------------------------------- | ------------------- | --------------------- | ----------------------------------------------- |
| **Native components** | Pre-built native components | Native (no browser) | Yes | Fastest integration |
@@ -34,7 +36,7 @@ Use the following tabs to choose your preferred approach:
**Native components**
-> Expo native components are currently in beta. If you run into any issues, please reach out to our [support team](https://clerk.com/support).
+> Expo native components are currently in beta. If you run into any issues, please reach out to the [support team](https://clerk.com/contact/support).
This approach uses Clerk's [pre-built native components](https://clerk.com/docs/reference/expo/native-components/overview.md) that render using SwiftUI on iOS and Jetpack Compose on Android. This requires the least code and a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
@@ -51,41 +53,27 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
cd clerk-expo
```
-3. ## Remove default template files
+3. ## Remove the starter routes
- The default Expo template includes files that will conflict with the routes you'll create in this guide. Remove the conflicting files:
+ The default Expo template includes starter routes that aren't used in this guide. Remove them:
```bash
- rm -rf "app/(tabs)" app/modal.tsx app/+not-found.tsx
+ rm -f src/app/index.tsx src/app/explore.tsx
```
- The default template also includes `react-native-reanimated`, which can cause [known Android build issues](https://docs.expo.dev/versions/latest/sdk/reanimated/#known-issues). Since it's not needed for this guide, remove it to avoid build errors:
-
- ```bash
- npm uninstall react-native-reanimated react-native-worklets --legacy-peer-deps
- ```
+ This guide replaces the starter `src/app/_layout.tsx` file and creates the app routes in later steps.
- Then, remove the reanimated import from `app/_layout.tsx`:
-
- filename: app/\_layout.tsx
-
- ```diff
- - import 'react-native-reanimated';
- ```
-
- > You can skip this step if you used `npx create-expo-app@latest --template blank` to create your app. However, the blank template doesn't include [Expo Router](https://docs.expo.dev/router/introduction/) or pre-styled UI components. You'll need to install `expo-router` and its dependencies to follow along with this guide.
+ > If your Expo app uses a root `app` folder instead of `src/app`, use the same file paths without `src/`.
4. ## Install dependencies
Install the required packages. Use `npx expo install` to ensure SDK-compatible versions.
- The [Clerk Expo SDK](https://clerk.com/docs/reference/expo/overview.md) gives you access to prebuilt components, hooks, and helpers to make user authentication easier.
- Clerk stores the active user's session token in memory by default. In Expo apps, the recommended way to store sensitive data, such as tokens, is by using `expo-secure-store` which encrypts the data before storing it.
- - `expo-auth-session` handles authentication redirects and OAuth flows in Expo apps.
- - `expo-web-browser` opens the system browser during authentication and returns the user to the app once the flow is complete.
- `expo-dev-client` allows you to build and run your app in development mode.
```bash
- npx expo install @clerk/expo expo-secure-store expo-auth-session expo-web-browser expo-dev-client
+ npx expo install @clerk/expo expo-secure-store expo-dev-client
```
5. ## Set your Clerk API keys
@@ -123,7 +111,7 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
Add the component to your root layout and pass your Publishable Key and `tokenCache` from `@clerk/expo/token-cache` as props, as shown in the following example:
- filename: app/\_layout.tsx
+ filename: src/app/\_layout.tsx
```tsx
import { ClerkProvider } from "@clerk/expo";
@@ -147,38 +135,33 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
8. ## Add authentication and home screen
- With [native components](https://clerk.com/docs/reference/expo/native-components/overview.md), you can build a complete app in a single file. The [ ](https://clerk.com/docs/reference/expo/native-components/auth-view.md) component handles all sign-in and sign-up flows, [ ](https://clerk.com/docs/reference/expo/native-components/user-button.md) provides a profile avatar that opens the native profile modal, and the [useUserProfileModal()](https://clerk.com/docs/reference/expo/native-components/user-profile-view.md) hook lets you open the profile modal from any button.
+ With [native components](https://clerk.com/docs/reference/expo/native-components/overview.md), you can build a complete app in a single file. The [ ](https://clerk.com/docs/reference/expo/native-components/auth-view.md) component handles all sign-in and sign-up flows, and [ ](https://clerk.com/docs/reference/expo/native-components/user-button.md) provides a profile avatar that opens the native user profile.
+
+ Create a `src/app/index.tsx` file with the following code. If the user is signed in, it displays the ` `. If they're not signed in, it displays a **Sign in** button that opens the ` `.
- Create an `index.tsx` file in your `app` folder with the following code. If the user is signed in, it displays their email, a profile button, and a sign-out button. If they're not signed in, it displays the ` ` component which handles both sign-in and sign-up.
+ > When using native components, pass `{ treatPendingAsSignedOut: false }` to [useAuth()](https://clerk.com/docs/expo/reference/hooks/use-auth.md) so pending session tasks are not treated as signed out.
- > When using native components, pass `{ treatPendingAsSignedOut: false }` to `useAuth()` to keep auth state in sync with the native SDK and avoid issues with pending session tasks.
+ > Keep the React Native `` that contains ` ` mounted at the same level as your signed-in and signed-out content. Don't render the modal only inside signed-out content, because auth state can change before required session tasks are finished and unmount the modal too early.
- filename: app/index.tsx
+ filename: src/app/index.tsx
```tsx
- import {
- useAuth,
- useUser,
- useClerk,
- useUserProfileModal,
- } from "@clerk/expo";
+ import { useAuth } from "@clerk/expo";
import { AuthView, UserButton } from "@clerk/expo/native";
+ import { useState } from "react";
import {
- Text,
View,
StyleSheet,
- Image,
- TouchableOpacity,
ActivityIndicator,
+ Button,
+ Modal,
} from "react-native";
export default function MainScreen() {
const { isSignedIn, isLoaded } = useAuth({
treatPendingAsSignedOut: false,
});
- const { user } = useUser();
- const { signOut } = useClerk();
- const { presentUserProfile } = useUserProfileModal();
+ const [isAuthOpen, setIsAuthOpen] = useState(false);
if (!isLoaded) {
return (
@@ -188,45 +171,21 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
);
}
- if (!isSignedIn) {
- return ;
- }
-
return (
-
- Welcome
-
-
-
-
-
- {user?.imageUrl && (
-
- )}
-
- Hello {user?.id}
-
-
-
- Manage Profile
-
- signOut()}
+ {isSignedIn ? (
+
+ ) : (
+ setIsAuthOpen(true)} />
+ )}
+ setIsAuthOpen(false)}
>
- Sign Out
-
+ setIsAuthOpen(false)} />
+
);
}
@@ -236,57 +195,12 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
flex: 1,
justifyContent: "center",
alignItems: "center",
- backgroundColor: "#fff",
- padding: 40,
},
container: {
flex: 1,
- backgroundColor: "#fff",
- padding: 20,
- paddingTop: 60,
- gap: 16,
- },
- header: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- },
- title: {
- fontSize: 28,
- fontWeight: "bold",
- },
- profileCard: {
- flexDirection: "row",
- alignItems: "center",
- padding: 16,
- backgroundColor: "#f5f5f5",
- borderRadius: 12,
- gap: 12,
- },
- avatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- },
- name: {
- fontSize: 18,
- fontWeight: "600",
- },
- email: {
- fontSize: 14,
- color: "#666",
- },
- linkButton: {
- backgroundColor: "#007AFF",
- padding: 16,
- borderRadius: 12,
+ justifyContent: "center",
alignItems: "center",
},
- linkButtonText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "600",
- },
});
```
@@ -318,7 +232,7 @@ This approach uses Clerk's [pre-built native components](https://clerk.com/docs/
10. ## Create your first user
Once the app opens on your device or simulator:
- - Navigate to the Sign up screen.
+ - Open the sign-up flow.
- Enter your details and complete the authentication flow.
- After signing up, your first user will be created and you'll be signed in.
@@ -364,29 +278,17 @@ This approach uses custom flows built with React Native components and **works i
cd clerk-expo
```
-3. ## Remove default template files
-
- The default Expo template includes files that will conflict with the routes you'll create in this guide. Remove the conflicting files:
+3. ## Remove the starter routes
- ```bash
- rm -rf "app/(tabs)" app/modal.tsx app/+not-found.tsx
- ```
-
- The default template also includes `react-native-reanimated`, which can cause [known Android build issues](https://docs.expo.dev/versions/latest/sdk/reanimated/#known-issues). Since it's not needed for this guide, remove it to avoid build errors:
+ The default Expo template includes starter routes that aren't used in this guide. Remove them:
```bash
- npm uninstall react-native-reanimated react-native-worklets --legacy-peer-deps
+ rm -f src/app/index.tsx src/app/explore.tsx
```
- Then, remove the reanimated import from `app/_layout.tsx`:
-
- filename: app/\_layout.tsx
-
- ```diff
- - import 'react-native-reanimated';
- ```
+ This guide replaces the starter `src/app/_layout.tsx` file and creates the app routes in later steps.
- > You can skip this step if you used `npx create-expo-app@latest --template blank` to create your app. However, the blank template doesn't include [Expo Router](https://docs.expo.dev/router/introduction/) or pre-styled UI components. You'll need to install `expo-router` and its dependencies to follow along with this guide.
+ > If your Expo app uses a root `app` folder instead of `src/app`, use the same file paths without `src/`.
4. ## Install dependencies
@@ -419,7 +321,7 @@ This approach uses custom flows built with React Native components and **works i
Add the component to your root layout and pass your Publishable Key and `tokenCache` from `@clerk/expo/token-cache` as props, as shown in the following example:
- filename: app/\_layout.tsx
+ filename: src/app/\_layout.tsx
```tsx
import { ClerkProvider } from "@clerk/expo";
@@ -443,7 +345,7 @@ This approach uses custom flows built with React Native components and **works i
7. ## Add sign-up and sign-in pages
- Clerk currently only supports [control components](https://clerk.com/docs/expo/reference/components/overview.md#control-components) for Expo native. [UI components](https://clerk.com/docs/expo/reference/components/overview.md) are only available for Expo web. Instead, you must build custom flows using Clerk's API. The following sections demonstrate how to build [custom email/password sign-up and sign-in flows](https://clerk.com/docs/guides/development/custom-flows/authentication/email-password.md). If you want to use different authentication methods, such as passwordless or OAuth, see the dedicated custom flow guides.
+ For JavaScript-only Expo native apps that run in Expo Go, use Clerk's [control components](https://clerk.com/docs/expo/reference/components/overview.md#control-components) and build custom flows with Clerk's API. [UI components](https://clerk.com/docs/expo/reference/components/overview.md) are available for Expo web, and [native components](https://clerk.com/docs/reference/expo/native-components/overview.md) are available for iOS and Android apps that use a development build. The following sections demonstrate how to build [custom email/password sign-up and sign-in flows](https://clerk.com/docs/guides/development/custom-flows/authentication/email-password.md). If you want to use different authentication methods, such as passwordless or OAuth, see the dedicated custom flow guides.
8. ### Layout page
@@ -451,7 +353,7 @@ This approach uses custom flows built with React Native components and **works i
1. Create an `(auth)` [route group](https://docs.expo.dev/router/advanced/shared-routes/). This will group your sign-up and sign-in pages.
2. In the `(auth)` group, create a `_layout.tsx` file with the following code. The [useAuth()](https://clerk.com/docs/expo/reference/hooks/use-auth.md) hook is used to access the user's authentication state. If the user is already signed in, they will be redirected to the home page.
- filename: app/(auth)/\_layout.tsx
+ filename: src/app/(auth)/\_layout.tsx
```tsx
import { useAuth } from "@clerk/expo";
@@ -476,7 +378,7 @@ This approach uses custom flows built with React Native components and **works i
In the `(auth)` group, create a `sign-up.tsx` file with the following code. The [useSignUp()](https://clerk.com/docs/expo/reference/hooks/use-sign-up.md) hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email.
- filename: app/(auth)/sign-up.tsx
+ filename: src/app/(auth)/sign-up.tsx
```tsx
import { ThemedText } from "@/components/themed-text";
@@ -731,7 +633,7 @@ This approach uses custom flows built with React Native components and **works i
In the `(auth)` group, create a `sign-in.tsx` file with the following code. The [useSignIn()](https://clerk.com/docs/expo/reference/hooks/use-sign-in.md) hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page.
- filename: app/(auth)/sign-in.tsx
+ filename: src/app/(auth)/sign-in.tsx
```tsx
import { ThemedText } from "@/components/themed-text";
@@ -1026,7 +928,7 @@ This approach uses custom flows built with React Native components and **works i
1. Create a `(home)` route group.
2. In the `(home)` group, create a `_layout.tsx` file with the following code.
- filename: app/(home)/\_layout.tsx
+ filename: src/app/(home)/\_layout.tsx
```tsx
import { useAuth } from "@clerk/expo";
@@ -1049,7 +951,7 @@ This approach uses custom flows built with React Native components and **works i
Then, in the same folder, create an `index.tsx` file. If the user is signed in, it displays their email and a sign-out button. If they're not signed in, it displays sign-in and sign-up links.
- filename: app/(home)/index.tsx
+ filename: src/app/(home)/index.tsx
```tsx
import { Show, useUser } from "@clerk/expo";
@@ -1123,7 +1025,7 @@ This approach uses custom flows built with React Native components and **works i
13. ## Create your first user
Once the app opens on your device or simulator:
- - Navigate to the Sign up screen.
+ - Open the sign-up flow.
- Enter your details and complete the authentication flow.
- After signing up, your first user will be created and you'll be signed in.
@@ -1139,9 +1041,9 @@ See the [`expo-updates`](https://docs.expo.dev/versions/latest/sdk/updates) libr
## Next steps
-Learn more about Clerk prebuilt components, custom flows for your native apps, and how to deploy an Expo app to production using the following guides.
+Learn more about Clerk prebuilt components, custom flows, and how to deploy an Expo app to production using the following guides.
- [Prebuilt native components (beta)](https://clerk.com/docs/reference/expo/native-components/overview.md): Learn how to quickly add authentication to your app using Clerk's pre-built native UI for iOS and Android.
-- [Custom flows](https://clerk.com/docs/guides/development/custom-flows/overview.md): Use custom flows in place of prebuilt components.
+- [Custom flows](https://clerk.com/docs/guides/development/custom-flows/overview.md): Build custom authentication flows with Clerk's API when prebuilt components don't fit your product.
- [Protect content and read user data](https://clerk.com/docs/expo/guides/users/reading.md): Learn how to use Clerk's hooks and helpers to protect content and read user data in your Expo app.
- [Deploy an Expo app to production](https://clerk.com/docs/guides/development/deployment/expo.md): Learn how to deploy your Expo app to production.
diff --git a/prompts/10e-profile-ui.md b/prompts/10e-profile-ui.md
new file mode 100644
index 0000000..5cc4526
--- /dev/null
+++ b/prompts/10e-profile-ui.md
@@ -0,0 +1,326 @@
+Read AGENTS.md first and follow it strictly.
+
+Reference: `assets/images/profile-main-ui.png`, `assets/images/profile-settings.png`, `AGENTS.md` (app structure), `prompts/09-bottom-tab-nav.md`, `prompts/05-clerk.md`, `prompts/08-zustand.md`, `prompts/10d-saved-ui.md`
+
+Implement the **Profile** tab and a **Settings** sub-screen exactly as shown in the attached designs. Match layout, typography, colors, radii, spacing, and icon treatments pixel-for-pixel — do not simplify either screen.
+
+Use assets from `assets/` via the centralized `constants/images.ts` import (`import { images } from "@/constants/images"`). Register any new image assets there before using them in screens or components.
+
+@assets/images/profile-main-ui.png
+
+@assets/images/profile-settings.png
+
+@assets/images/profile-avatar.png
+
+## Goal
+
+Replace the Profile tab placeholder (`app/(tabs)/profile.tsx`) with two connected screens:
+
+1. **Profile hub** — hero header, user identity, travel stats, and navigation rows (Saved places, Visited countries, Travel map, Settings)
+2. **Settings** — compact account header, stats bar, preferences list, and Log out
+
+The Profile tab is the user's home for identity, discovery progress, and app preferences. Wire real data from Clerk and existing Zustand stores where possible; use sensible local defaults for stats the app does not track yet.
+
+## Prerequisites
+
+- `prompts/09-bottom-tab-nav.md` — `(tabs)` routes and `components/bottom-tab-bar.tsx` (Explore, Culture, Map, Saved, Profile)
+- `prompts/05-clerk.md` — Clerk auth wired; signed-in users reach `(tabs)` via `useAuth()`
+- `prompts/08-zustand.md` — feed, saved, and discovery progress stores
+- `prompts/01-nativewind.md` and `prompts/02-design-theme.md` — theme tokens in `global.css`, Poppins fonts
+- `prompts/10d-saved-ui.md` — Saved tab for **Saved places** navigation target
+
+## Dependencies
+
+Use what is already installed:
+
+- `expo-router`, `expo-image`
+- `@clerk/expo` (`useUser`, `useClerk` for sign out)
+- `zustand`, `@react-native-async-storage/async-storage`
+- `@expo/vector-icons` (Ionicons)
+- `react-native-safe-area-context`
+
+Do **not** add chart libraries, React Query, or new navigation libraries without user approval.
+
+## Route & files
+
+| Path | Purpose |
+| ------------------------------------------------ | ------------------------------------------------------------------------------------- |
+| `app/(tabs)/profile.tsx` | Profile hub screen — composes hero, stats, navigation rows |
+| `app/profile/settings.tsx` | Settings sub-screen — preferences list + log out (stack route, tab bar still visible) |
+| `components/profile/profile-hero-header.tsx` | Scenic hero background, avatar, name, handle, bio, inline stats, Edit profile button |
+| `components/profile/profile-travel-stats.tsx` | Four-card travel stats grid (Countries, Cities, Places, Continents) |
+| `components/profile/profile-nav-row.tsx` | Reusable row: icon, title, subtitle, trailing thumbnail or flag strip |
+| `components/profile/profile-settings-header.tsx` | Compact header: avatar + edit badge, name, email, location |
+| `components/profile/profile-stats-bar.tsx` | Three-column stats bar (Countries, Saved, Photos) |
+| `components/profile/profile-settings-row.tsx` | Settings list row — icon, label, optional value, chevron or toggle |
+| `components/profile/profile-log-out-button.tsx` | Full-width destructive log out button |
+| `store/use-profile-settings-store.ts` | Local preferences: dark mode toggle, selected language (persisted) |
+| `constants/images.ts` | Export `profileAvatar`, hero/thumbnail assets used in rows |
+
+Keep business logic in stores/hooks; screens orchestrate layout and navigation only.
+
+Suggested stack registration in root `app/_layout.tsx`:
+
+```tsx
+
+```
+
+## Screen 1 — Profile hub (`profile-main-ui.png`)
+
+### Shell
+
+- **Background:** deep space navy (`SPACE_SCREEN_BASE` / `#0b132b`) with hero image overlay at top (~40% of screen height)
+- **Safe area:** `SafeAreaView` with inline `backgroundColor` (StyleSheet — not NativeWind on SafeAreaView)
+- **Scroll:** vertical `ScrollView` with bottom padding clearing the tab bar (`TAB_BAR_CONTENT_HEIGHT + insets.bottom + 16`)
+- **Status bar:** light content (`expo-status-bar`)
+
+### Top chrome
+
+| Element | Spec |
+| ------- | -------------------------------------------------------------------------------------------------- |
+| Left | Hamburger menu icon (`Ionicons` `menu-outline`) — v1: no-op or dev drawer stub |
+| Center | **WorldLoop** title, white, semibold |
+| Right | Search icon (`search-outline`) — wire to existing `SearchOverlay` if trivial; otherwise no-op stub |
+
+Match icon size (~24px), hit slop, and vertical alignment from the reference.
+
+### Hero / identity block
+
+| Element | Spec |
+| ---------------- | -------------------------------------------------------------------------------------------------------- |
+| Avatar | Large circle (~96–104px), `expo-image`, border optional |
+| Name | Bold white, e.g. **Alex Traveler** |
+| Handle | Muted gray `@username` below name |
+| Bio | One line, white/80 — e.g. _Exploring the world, one destination at a time._ |
+| Inline stats row | Three columns: **24** COUNTRIES · **56** CITIES · **128** PLACES — uppercase labels, gold accent numbers |
+| Edit profile | Outlined pill, pencil icon + **Edit profile** — v1 navigates to Settings or shows “Coming soon” toast |
+
+**Clerk wiring:**
+
+- Name: `user.fullName` or `firstName` fallback
+- Handle: `@` + slug from `user.username` or email local-part
+- Bio: `user.unsafeMetadata.bio` if set; else design default copy
+- Avatar: `user.imageUrl` → fallback `images.profileAvatar`
+
+### Travel stats section
+
+Section header: gold line-graph icon + **Travel stats** label.
+
+Four equal cards in a horizontal row (wrap or horizontal scroll on narrow screens):
+
+| Card | Icon | Value source (v1) |
+| ---------- | ------------ | ---------------------------------------------------------------------------------------- |
+| Countries | globe | `useDiscoveryProgressStore.countriesExplored` |
+| Cities | location pin | derived: `Math.round(countriesExplored * 2.3)` or static `56` until city tracking exists |
+| Places | camera | `useSavedCountriesStore.savedCountries.length` or static `128` |
+| Continents | flag | count unique continents from visited + saved country `region` fields via feed store |
+
+Card styling: dark rounded rectangle (`~12–16px` radius), gold icon top-left, white value, muted label.
+
+### Navigation rows
+
+Four full-width pressable rows inside subtle dark cards. Each row: left icon (gold), title + subtitle, trailing visual, chevron right.
+
+| Row | Title | Subtitle | Trailing | Navigation |
+| ----------------- | --------------------- | ----------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
+| Saved places | **Saved places** | View all your saved destinations | landscape thumbnail | `router.push("/(tabs)/saved")` |
+| Visited countries | **Visited countries** | `{n} countries explored` | 3 flag circles + `+{rest}` badge | v1: `router.push("/(tabs)/map")` with visited filter stub, or dedicated list route later |
+| Travel map | **Travel map** | See everywhere you've been | world map thumbnail with pins | `router.push("/(tabs)/map")` |
+| Settings | **Settings** | Manage your account and preferences | none | `router.push("/profile/settings")` |
+
+**Trailing thumbnails:** use `images.earthMap` or first saved country's hero image from feed-enriched saved store. Flag strip: use `FlagBadge` / flagcdn for top 3 visited countries by `visitedAt`.
+
+Row press: subtle opacity feedback; optional light haptic.
+
+## Screen 2 — Settings (`profile-settings.png`)
+
+Push route from Profile hub **Settings** row. Tab bar remains visible (Profile tab stays active).
+
+### Header
+
+| Element | Spec |
+| -------- | -------------------------------------------------------------------------------------------- |
+| Title | **WorldLoop** centered (same chrome as hub, or back chevron left + title) |
+| Right | Gear icon (decorative — user is already on settings) |
+| Avatar | Smaller circle (~64px) with pencil edit badge overlay bottom-right |
+| Name | **Alex Morgan** — from Clerk |
+| Email | muted — `user.primaryEmailAddress?.emailAddress` |
+| Location | pin icon + **San Francisco, USA** — v1: static design copy or `user.unsafeMetadata.location` |
+
+### Stats bar
+
+Single dark card, three columns:
+
+| Stat | Icon | Value source |
+| --------- | ---------------- | ------------------------------------ |
+| Countries | passport / globe | `countriesExplored` |
+| Saved | bookmark | `savedCountries.length` |
+| Photos | camera | v1 static `128` or saved count proxy |
+
+Gold icons, white numbers, muted labels.
+
+### Settings list
+
+One rounded dark container with dividers between rows.
+
+| Row | Icon | Behavior |
+| -------------------- | ----------- | -------------------------------------------------------------------------------------- |
+| Personal information | person | v1 stub — navigate to placeholder or Clerk account management note |
+| Notifications | bell | v1 stub |
+| Offline maps | download | v1 stub |
+| Language | globe | shows **English** on right; tap → `/(language)` or existing language screen if present |
+| Dark mode | moon | **Switch** toggle (yellow when on) — persist in `useProfileSettingsStore` |
+| Help center | help-circle | v1 stub (external link placeholder) |
+| Privacy & terms | shield | v1 stub |
+
+Toggle rows use `Switch` with gold track when enabled — StyleSheet for switch colors if NativeWind cannot style it.
+
+### Log out
+
+Full-width button below the list, separate card:
+
+- Red icon (`log-out-outline`) + **Log out** label in red
+- Calls `signOut()` from `useClerk()`, then `router.replace("/onboarding")` or auth entry route
+- Confirm with `Alert.alert` optional — keep simple for v1
+
+## Data wiring
+
+### Clerk (`useUser`, `useClerk`)
+
+```tsx
+const { user } = useUser();
+const { signOut } = useClerk();
+```
+
+| UI field | Clerk source |
+| --------------- | ------------------------------------------------- |
+| Display name | `user?.fullName ?? user?.firstName ?? "Traveler"` |
+| Email | `user?.primaryEmailAddress?.emailAddress` |
+| Avatar | `user?.imageUrl` |
+| Username handle | `user?.username` |
+
+Do not expose secret keys. Profile edits that require server writes go through Clerk user update APIs in a later lesson.
+
+### Discovery progress (`useDiscoveryProgressStore`)
+
+| Profile UI | Store field |
+| ------------------------------ | --------------------------------------------------------------- |
+| Countries explored count | `countriesExplored` |
+| Visited countries row subtitle | `countriesExplored` |
+| Visited flag strip | map `visitedCountryIds` → feed/saved countries for `cca2` flags |
+| Continents card | derive from visited countries' `region` via feed enrichment |
+
+### Saved countries (`useSavedCountriesStore`)
+
+| Profile UI | Store field |
+| -------------------------- | ------------------------------------------ |
+| Saved places row thumbnail | first saved country's image |
+| Saved stat (settings) | `savedCountries.length` |
+| Places stat proxy | `savedCountries.length` or static fallback |
+
+### Profile settings store (new)
+
+Create `store/use-profile-settings-store.ts`:
+
+| Field | Type | Default | Persist |
+| ----------------- | --------- | --------------------- | --------------------------------------------- |
+| `darkModeEnabled` | `boolean` | `true` (match design) | AsyncStorage key `worldloop-profile-settings` |
+| `languageCode` | `string` | `"en"` | same |
+
+v1: dark mode toggle updates store only — app-wide theme switching is optional follow-up; toggle must reflect stored value on reopen.
+
+## Styling rules
+
+- **NativeWind** `className` for typography on `Text`, layout on `View`/`Pressable` where supported
+- **StyleSheet** for: `SafeAreaView`, `Switch`, hero image absolute positioning, shadows, row thumbnail sizes, avatar edit badge overlay
+- **Colors:** dark navy background, card fill `~#1a2235`, gold accent `#fbbf24` (`tab-active`), muted text `white/55`, destructive red `#ef4444`
+- **Spacing:** 8-point grid (8, 16, 24, 32)
+- **Radii:** cards ~12–16px; avatar full circle; stats cards ~12px
+- Do **not** put `className` on `SafeAreaView`
+
+## Out of scope (v1)
+
+- Full edit-profile form (name, bio, photo upload)
+- Push notification permissions flow
+- Offline map downloads
+- Cloud sync of profile stats beyond Clerk identity
+- Backend profile API (`backend/src/controllers/profile.controller.ts` is for **country** profiles, not user profiles)
+- In-screen search/filter on visited countries list
+- Real photo count tracking
+
+## Acceptance criteria
+
+- Profile tab matches `profile-main-ui.png` — hero, identity, inline stats, travel stats grid, four navigation rows
+- Tapping **Settings** opens settings screen matching `profile-settings.png`
+- Tapping **Saved places** navigates to Saved tab
+- Tapping **Travel map** navigates to Map tab
+- Countries and Saved counts reflect live Zustand store values (not hardcoded when data exists)
+- Clerk user name, email, and avatar appear when signed in; sensible fallbacks when missing
+- Dark mode toggle persists across app restarts
+- Log out signs out via Clerk and returns user to unauthenticated entry
+- `images.profileAvatar` and other assets imported through `constants/images.ts`
+- `npm run lint` and `npm run typecheck` pass
+
+## Testing
+
+```bash
+# Terminal 1 — backend (optional for profile v1)
+# Terminal 2
+npx expo start
+```
+
+1. Sign in and open **Profile** tab — hub matches reference layout
+2. Confirm inline stats and travel stats cards show store-driven country count after visiting countries in Explore
+3. Save countries from Explore — **Saved places** row subtitle/thumbnail updates; settings **Saved** stat updates
+4. Tap **Saved places** → Saved tab opens
+5. Tap **Travel map** → Map tab opens
+6. Tap **Settings** → settings screen opens with list + toggle
+7. Toggle dark mode off/on — restart app — toggle state persists
+8. Tap **Log out** — session cleared, redirected to onboarding/auth
+9. Dev: `__DEV__` onboarding reset link can remain on profile or move to settings footer
+
+## Architecture diagram
+
+```mermaid
+flowchart TB
+ subgraph ProfileTab["app/(tabs)/profile.tsx"]
+ Hero["ProfileHeroHeader"]
+ Stats["ProfileTravelStats"]
+ Rows["ProfileNavRows"]
+ end
+
+ subgraph Settings["app/profile/settings.tsx"]
+ SHeader["ProfileSettingsHeader"]
+ SBar["ProfileStatsBar"]
+ SList["ProfileSettingsRows"]
+ LogOut["ProfileLogOutButton"]
+ end
+
+ subgraph Data
+ Clerk["useUser / useClerk"]
+ Discovery["useDiscoveryProgressStore"]
+ Saved["useSavedCountriesStore"]
+ Prefs["useProfileSettingsStore"]
+ end
+
+ Hero --> Clerk
+ Stats --> Discovery
+ Stats --> Saved
+ Rows -->|"Settings row"| Settings
+ Rows -->|"Saved places"| SavedTab["/(tabs)/saved"]
+ Rows -->|"Travel map"| MapTab["/(tabs)/map"]
+
+ SHeader --> Clerk
+ SBar --> Discovery
+ SBar --> Saved
+ SList --> Prefs
+ LogOut --> Clerk
+```
+
+## Next steps
+
+1. Edit profile screen — update Clerk `unsafeMetadata` (bio, location)
+2. Visited countries list route with flag grid and explore CTA
+3. Wire dark mode toggle to app-wide color scheme
+4. Notifications + offline maps flows when backend support exists
+5. Replace static Cities / Photos / Continents with real tracking stores
diff --git a/skills-lock.json b/skills-lock.json
index efb1652..8b1cdf4 100644
--- a/skills-lock.json
+++ b/skills-lock.json
@@ -7,6 +7,48 @@
"skillPath": "plugins/expo/skills/building-native-ui/SKILL.md",
"computedHash": "a0316d2ce842d2dd8067a144e339cf502329dc7701f9f5122268fdb3d4c980f7"
},
+ "clerk": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/core/clerk/SKILL.md",
+ "computedHash": "9a7ea0a999381471bb8f783260b295360a2a937d61c9d1c3163cede96b39c229"
+ },
+ "clerk-backend-api": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/core/clerk-backend-api/SKILL.md",
+ "computedHash": "cbcad1341a7d32fed789abe864c829f27a2858dc8ba68b92254e9beb53467148"
+ },
+ "clerk-cli": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/core/clerk-cli/SKILL.md",
+ "computedHash": "65eb4d512aabfac55c7bf91cdc1edbac10cbd34dfeb815cbfa0fa7a1ff35cd5c"
+ },
+ "clerk-custom-ui": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/core/clerk-custom-ui/SKILL.md",
+ "computedHash": "b45ddd91c4924e8a74cb5272e769b06a5bbbde329983e7e565e6902b1cc80ded"
+ },
+ "clerk-expo-patterns": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/frameworks/clerk-expo-patterns/SKILL.md",
+ "computedHash": "13911c9c4bd9fa47a1f2812d7e980aca269fd503987e91996fe779fc6e8ac262"
+ },
+ "clerk-setup": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/core/clerk-setup/SKILL.md",
+ "computedHash": "bbf0711ddcc8d8d0dcd148c0061ab83c7d66225a60a28caba62ef76e4384fa54"
+ },
+ "clerk-webhooks": {
+ "source": "clerk/skills",
+ "sourceType": "github",
+ "skillPath": "skills/features/clerk-webhooks/SKILL.md",
+ "computedHash": "4764eeeef1e80ce9757759ef119a3a5e77322383e8a37975d7c8ea718c259ec9"
+ },
"eas-update-insights": {
"source": "expo/skills",
"sourceType": "github",
diff --git a/store/index.ts b/store/index.ts
index 00fb862..89d87dd 100644
--- a/store/index.ts
+++ b/store/index.ts
@@ -5,6 +5,7 @@ export { useIdentityStore } from "./use-identity-store";
export type { SelectionSource } from "./use-identity-store";
export { useMapStore } from "./use-map-store";
export { useOnboardingStore } from "./use-onboarding-store";
+export { useProfileSettingsStore } from "./use-profile-settings-store";
export { useSavedCountriesStore } from "./use-saved-countries-store";
export { useSearchUiStore } from "./use-search-ui-store";
export { useSpatialContextStore } from "./use-spatial-context-store";
diff --git a/store/use-profile-settings-store.ts b/store/use-profile-settings-store.ts
new file mode 100644
index 0000000..e16d862
--- /dev/null
+++ b/store/use-profile-settings-store.ts
@@ -0,0 +1,34 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { create } from "zustand";
+import { createJSONStorage, persist } from "zustand/middleware";
+
+type ProfileSettingsState = {
+ darkModeEnabled: boolean;
+ languageCode: string;
+ setDarkModeEnabled: (enabled: boolean) => void;
+ setLanguageCode: (code: string) => void;
+};
+
+export const useProfileSettingsStore = create()(
+ persist(
+ (set) => ({
+ darkModeEnabled: true,
+ languageCode: "en",
+ setDarkModeEnabled: (enabled) => set({ darkModeEnabled: enabled }),
+ setLanguageCode: (code) => set({ languageCode: code }),
+ }),
+ {
+ name: "worldloop-profile-settings",
+ storage: createJSONStorage(() => AsyncStorage),
+ },
+ ),
+);
+
+export function getLanguageLabel(code: string): string {
+ const labels: Record = {
+ en: "English",
+ es: "Spanish",
+ fr: "French",
+ };
+ return labels[code] ?? "English";
+}