Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6151d12
chore(repo): add the verify-clerk-expo skill's package and repo wiring
mikepitre Oct 6, 2026
00e4daf
chore(repo): add the verify-clerk-expo skill's lockfile
mikepitre Oct 6, 2026
80f0652
test(expo): add the verify code shared with clerk-ios and clerk-android
mikepitre Oct 6, 2026
41a354a
test(expo): add the Expo host, the fixture build, the Metro freshness…
mikepitre Oct 6, 2026
ae77d62
docs(expo): add the verify-clerk-expo skill docs and point agents at …
mikepitre Oct 6, 2026
be823ac
ci(repo): run the verify-clerk-expo unit tests and typecheck
mikepitre Oct 6, 2026
af7c5b5
chore(repo): move the verify-clerk-expo skill to e2e 0.18.0 and @e2e-…
mikepitre Oct 6, 2026
0a461c4
test(expo): tap with a plain locator tap in the verify specs
mikepitre Oct 6, 2026
880d142
test(expo): add --retries to the verify run and keep the failed attem…
mikepitre Oct 6, 2026
01505b6
test(expo): add run --github-report, which reports a verify run throu…
mikepitre Oct 6, 2026
e8d16e7
test(expo): write junit.xml beside each e2e report of a verify run
mikepitre Oct 6, 2026
d006d94
test(expo): make host.fill confirm that the text reached the field
mikepitre Oct 6, 2026
fe78653
test(expo): keep the verify skill unit tests out of an inherited git …
mikepitre Oct 6, 2026
71e458d
test(expo): turn off animations and system error dialogs on the Andro…
mikepitre Oct 6, 2026
a853b75
test(expo): add the remote device code shared with clerk-ios and cler…
mikepitre Oct 6, 2026
f668f22
test(expo): borrow a simulator or emulator on a CI runner for the ver…
mikepitre Oct 6, 2026
04c62e6
docs(expo): add the remote device to the verify-clerk-expo docs
mikepitre Oct 6, 2026
b2758a8
ci(repo): pin the iOS runtime of the simulator a verify-remote sessio…
mikepitre Oct 6, 2026
b0ca517
test(expo): borrow a device on the free GitHub-hosted labels by default
mikepitre Oct 6, 2026
4ebd2f9
test(expo): take the trimmed verify code shared with clerk-ios
mikepitre Oct 6, 2026
93308ec
test(expo): read a session's request on a free runner whatever its label
mikepitre Oct 6, 2026
05cc044
test(expo): give a borrowed device 40 minutes to be ready
mikepitre Oct 6, 2026
44e8bb3
test(expo): keep a run's results when its video is lost, and four sma…
mikepitre Oct 6, 2026
4742d0c
test(expo): take a lock whose file cannot be read as an owner
mikepitre Oct 6, 2026
3c093a4
test(expo): tap a field again when the first tap left nothing focused
mikepitre Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/expo-verify-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions .changeset/expo-verify-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
18 changes: 11 additions & 7 deletions .claude/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ Edits to a `SKILL.md` take effect immediately, including in already-running sess

## Scope

Skills are Claude Code specific. Cursor does not read this directory; it uses `.cursor/rules/` and
`AGENTS.md`. When a repo rule changes, update `AGENTS.md` first, then mirror the change here and in
`.cursor/rules/` where relevant.
Skills here are Claude Code specific, except `verify-clerk-expo`, the agent-neutral skill that drives
`@clerk/expo` on a simulator or emulator. Its files live here, and `.cursor/skills/verify-clerk-expo`
is a symlink to this directory so Cursor reads the same skill. Edit it here, not through the
symlink. For the other skills, Cursor uses `.cursor/rules/` and `AGENTS.md`. When a repo rule
changes, update `AGENTS.md` first, then mirror the change here and in `.cursor/rules/` where
relevant.

## Maintaining a skill

Expand All @@ -41,7 +44,8 @@ Skills are Claude Code specific. Cursor does not read this directory; it uses `.

## Skills in this repo

| Skill | Use it for |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. |
| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). |
| Skill | Use it for |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. |
| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). |
| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, local or borrowed on a CI runner, with video and app state as evidence. |
3 changes: 3 additions & 0 deletions .claude/skills/verify-clerk-expo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.e2e/
.verify/
specs/explored/
262 changes: 262 additions & 0 deletions .claude/skills/verify-clerk-expo/SKILL.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions .claude/skills/verify-clerk-expo/bin/control-clerk-expo
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env node
import { ensureRuntime } from '../src/core/launch.mjs';

await ensureRuntime();
const { main } = await import('../src/core/cli.ts');
const { host } = await import('../src/host.ts');
process.exitCode = await main(process.argv.slice(2), host);
3 changes: 3 additions & 0 deletions .claude/skills/verify-clerk-expo/e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { composeE2EConfig, loadRunContext } from './src/core/e2e-config.ts';

export default composeE2EConfig(loadRunContext());
73 changes: 73 additions & 0 deletions .claude/skills/verify-clerk-expo/features/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# @clerk/expo verification map

This directory is the maintained source for verifying the user-facing behavior of `@clerk/expo` through the `expo-native` fixture app in `integration/templates/expo-native`. On a local device the fixture runs as a Debug dev client that loads its JS, and `packages/expo` with it, from a local Metro server. On a remote device it runs as a standalone Release build of a pushed commit, with the JS embedded. Read this index before driving the app, then use the matching feature file as the recipe. Every recipe runs through `.claude/skills/verify-clerk-expo/bin/control-clerk-expo` and the golden specs under `specs/golden/<feature>/`.

## Baseline preconditions

- Run commands from the root of a clerk/javascript worktree. Run `pnpm install` and `npm ci --prefix .claude/skills/verify-clerk-expo` once there. Paths that start with `.verify/` or `specs/` are inside the skill directory.
- Run `.claude/skills/verify-clerk-expo/bin/control-clerk-expo doctor --platform ios` (or `android`) first. Before the first `up`, `build` is the one failing check. On a machine that cannot run the device, `doctor` checks the path to a remote device instead. A remote device runs a build of a pushed commit, so commit and push before `up` and before a `run` that follows an edit.
- `up` needs the team's Clerk Platform API key: `CLERK_PLATFORM_API_KEY` or `CLERK_PLATFORM_API_KEY_FILE`, a cloud environment's API credential, or a 1Password reference that the machine holds outside the repository. `doctor` names the one it found in its `instances` line and prints one fix line when there is none. Do not read or print the key.
- The CLI drives only devices it owns: a local lane, or a remote simulator or emulator that it leased on a CI runner. On iOS a lane is `verify-ios-<n>`, cloned from `Clerk Verify Template iOS`. On Android it is `Clerk_Verify_Pixel` booted `-read-only` as `emulator-5560` or `emulator-5562`. Never drive a simulator or emulator the CLI did not create, or a device another worktree holds.
- Every launch gets a new `verifyStorageScope` unless it passes `keepStorage: true`, so no spec inherits a session from another spec.

## Test users and sign-in

A change that touches sign-in or sign-up gets a spec that drives the real form. Anything else reaches a signed-in state with a ticket (`host.launch({ signedInAs })`). SKILL.md has the rule, and these are the identifiers a spec needs.

- **Emails.** Any address that contains `+clerk_test@` is a test address. Clerk sends no mail and accepts the code below. `host.newEmail()` and `host.seedUser()` mint `verify_<runId>_<n>+clerk_test@example.com`, new per run.
- **One-time code.** `424242` verifies every email code for a test address. Specs use the constant `CLERK_TEST_CODE`.
- **Passwords.** The standard settings require a password at sign-up. Use a throwaway per run, such as `Verify-<runId>-Pw1!`.

| Step | Native AuthView (iOS identifier) | Custom flow (fixture testID) |
| ------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------- |
| Email field | `clerk.auth.start.identifier` | `verify.customSignIn.emailAddress`, `verify.customSignUp.emailAddress` |
| Switch between email and phone | `clerk.auth.start.identifierSwitcher` | none |
| Phone field | `clerk.auth.start.phoneNumber` | none |
| Continue or send code | `clerk.auth.start.continue` | `verify.customSignIn.sendCode`, `verify.customSignUp.sendCode` |
| Try another method on the email link screen | the `Use another method` text | none |
| Pick a method from the list | `clerk.auth.signIn.alternativeMethod.<strategy>` | none |
| Sign-in code field | `clerk.auth.signIn.code` | `verify.customSignIn.code`, then `verify.customSignIn.verifyCode` |
| Sign-up password | `clerk.auth.signUp.password` | `verify.customSignUp.password` |
| Sign-up code field | `clerk.auth.signUp.code` | `verify.customSignUp.code`, then `verify.customSignUp.verifyCode` |
| Close AuthView | `clerk.dismissButton` | none |

The iOS identifiers come from `Sources/ClerkKitUI/Components/Auth/ClerkAccessibilityIdentifiers.swift` in the clerk-ios release that `packages/expo/ios/ClerkExpo.podspec` pins. The clerk-android release that `@clerk/expo` pins has no test tags, so Android specs find native views by their text. The custom-flow testIDs live in `integration/templates/expo-native/screens/`.

Rules:

- Type only `+clerk_test` emails and `424242`. The repo is public and every video can land on a PR.
- Use ticket sign-in only to reach signed-in screens for features that are not about authentication.
- Tag every spec that types a code or a password `form-entry`. Those specs run by default. A runtime that cannot type codes into the app passes `--skip form-entry` to `run` and says so in the PR.
- `down` deletes the worktree's application and every user in it, including users created through the sign-up form.

## Driving conventions

- Input reaches the app only through specs. To look at a state past launch, write a spec, `run` it, then run `screen`.
- `verifyScreen` routes the host: `home` (the fixture's own screen), `auth` (AuthView with no close button), `nativeAuth` (AuthView with a close button), `userButton`, `userProfile`, `customSignIn`, `customSignUp`, and `tokenCache`. `state.screen` reports what is on screen: `launching` during a ticket sign-in, `error` for a rejected key.
- Prefer SDK identifiers on iOS and fixture testIDs everywhere. `specs/native.ts` holds the per-platform locators for native views that have no Android tag yet.
- Fill the text fields of the native views with `host.fill(locator, text)`, which taps the field and types into it. On iOS a native text field shows no text input until it has focus, and until then its identifier is on the floating label, so a plain `locator.fill()` fails with "no text input found at the provided coordinates to clear". A tap needs no helper: `host.tap(locator)` is `locator.tap()` with the assertion timeout.
- `host.fill` reads the focused input back and types once more if it is still empty. It cannot confirm a password field, which withholds its value, or on iOS a React Native field of the fixture, which reads as its placeholder while it is empty. Those are typed once.
- Prove results from `verify.state` (`host.launch`, `host.state`, `host.waitForState`), not from the screen alone. Its fields are `v`, `screen`, `environmentLoaded`, `signedIn`, `userId`, `sessionId`, `sessionStatus`, `pendingTasks`, `orgId`, `signInStatus`, `signUpStatus`, `ticket`, `lastError`, `runId`, `launchId`, and `extra`. `v` is the version of the state contract. The fixture puts `authViewLoaded` and `authFlowComplete` from `useAuthViewState` in `extra`. Every spec keeps at least one exact assertion on `verify.state` or an SDK identifier.

## Proof and skip reporting

- A proof is a passing `run` whose run directory holds `video.mp4`, `screenshots/`, `states.jsonl`, `state.json`, `app.log`, and `e2e/report.json`. `states.jsonl` is the state proof on both platforms. On Android `app.log` also has the app's `[verify]` console lines. On a local iOS device it has native log lines only, and the JS console lines, a JS change's own log lines included, are in `.verify/runtime/metro-<port>.log`. Read this run's part of that log by `runId`, as `references/freshness.md` describes. A remote device has no Metro log, so prove a JS change there from `states.jsonl` and a screenshot.
- Name the run id, the platform, and the specs in the PR. Attach the run with `attach <run-id> --pr <n>`.
- Four specs carry `form-entry`: `custom-flow-sign-in/complete`, `custom-flow-sign-up/request-code`, `custom-flow-sign-up/complete`, and `native-auth-view/complete`. None has a recorded passing run. Report a skipped one as skipped with the reason the CLI prints, `skipped by --skip form-entry`, and never as verified through a ticket launch.
- `custom-flow-sign-in/request-code` and `native-auth-view/request-code` (iOS) type no code or password, so `--skip form-entry` still runs them and they prove their flow up to the code screen. Sign-up has no such spec.
- A spec limited to one platform reports as skipped on the other. Say which platform, and local or remote device, each proof ran on.

## Feature entry contract

Each feature file starts with an H1 title and one paragraph describing the user-visible behavior, then exactly four H2 sections in this order: `Sub-features`, `How to get to it (user POV)`, `Driving it with verify`, and `Gotchas`. `Driving it with verify` starts with `Preconditions:` and names the golden specs that prove each sub-feature. A feature file lists only entry points that a spec drives.

## Features

- [Native AuthView](./native-auth-view.md) covers opening, dismissing, and signing in through the native AuthView.
- [User button and profile](./user-button-and-profile.md) covers the native UserButton and UserProfileView.
- [Custom sign-in flow](./custom-flow-sign-in.md) covers an email code sign-in built on `useSignIn`.
- [Custom sign-up flow](./custom-flow-sign-up.md) covers an email and password sign-up built on `useSignUp`.
- [Token cache persistence](./token-cache-persistence.md) covers restoring the session after a relaunch.
- [Native and JS session sync](./native-js-sync.md) covers a native sign-out reaching the JS hooks.

Not mapped yet: SSO through `useSSO` (no real OAuth on simulators), Google and Apple native sign-in, passkeys and biometrics (no associated domains or Secure Enclave on the simulator), and session tasks in the native views.
29 changes: 29 additions & 0 deletions .claude/skills/verify-clerk-expo/features/custom-flow-sign-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Custom sign-in flow

An app that builds its own sign-in screen on `useSignIn` sends an email code to an existing user and signs them in with it.

## Sub-features

- `request-code` sends the email code and shows the code field.
- `complete` verifies the code, finalizes the sign-in, and reports an active session.

## How to get to it (user POV)

- Launch `verifyScreen customSignIn`. The screen has an email field, `Send code`, then a code field and `Verify code`.

## Driving it with verify

Preconditions:

- The standard settings turn the `email_code` strategy on. `up` fails with `INSTANCE_MISCONFIGURED` when the application does not show it.
- The spec seeds its own `+clerk_test` user.

- **Request the code.** Run `.claude/skills/verify-clerk-expo/bin/control-clerk-expo run custom-flow-sign-in/request-code`. The spec fills `verify.customSignIn.emailAddress`, taps `verify.customSignIn.sendCode`, expects `verify.customSignIn.code`, and waits for `signInStatus` `needs_first_factor`. Screenshot `custom-code`.
- **Enter the code.** Run `.claude/skills/verify-clerk-expo/bin/control-clerk-expo run custom-flow-sign-in/complete` (tag `form-entry`, no recorded passing run). It types `CLERK_TEST_CODE`, taps `verify.customSignIn.verifyCode`, and waits for `signedIn` true with the seeded `userId`. Screenshots `custom-code` and `custom-signed-in`.
- **Proof.** Both specs pass on the platform you changed. A runtime that must use `--skip form-entry` proves `request-code` only and reports `complete` as skipped.

## Gotchas

- These are React Native views with `testID`s, so plain locator actions work. `host.tap` and `host.fill` also work and keep specs uniform. On iOS `host.fill` cannot confirm that the text reached one of these fields, because an empty one reads as its placeholder.
- A sign-in error shows in `verify.customSignIn.error`. It does not reach `lastError`. Read the error text from the failure page when a spec stops on the email screen.
- The screen keeps no state across launches. Every launch starts on the email field.
29 changes: 29 additions & 0 deletions .claude/skills/verify-clerk-expo/features/custom-flow-sign-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Custom sign-up flow

An app that builds its own sign-up screen on `useSignUp` creates a user with an email address and password, then verifies the email code.

## Sub-features

- `request-code` creates the sign-up and shows the code field.
- `complete` verifies the code, finalizes the sign-up, and reports an active session.

## How to get to it (user POV)

- Launch `verifyScreen customSignUp`. The screen has email and password fields, `Send code`, then a code field and `Verify code`.

## Driving it with verify

Preconditions:

- The standard settings require a password at sign-up. The spec uses `Verify-<runId>-Pw1!`.
- The spec reserves a new `+clerk_test` address with `host.newEmail`, so `.claude/skills/verify-clerk-expo/bin/control-clerk-expo down` deletes the user the form creates along with the application.

- **Request the code.** Run `.claude/skills/verify-clerk-expo/bin/control-clerk-expo run custom-flow-sign-up/request-code` (tag `form-entry`, because it types a password, and no recorded passing run). The spec fills `verify.customSignUp.emailAddress` and `verify.customSignUp.password`, taps `verify.customSignUp.sendCode`, expects `verify.customSignUp.code`, and waits for `signUpStatus` `missing_requirements`. Screenshot `custom-signup-code`.
- **Enter the code.** Run `.claude/skills/verify-clerk-expo/bin/control-clerk-expo run custom-flow-sign-up/complete` (tag `form-entry`, no recorded passing run). It types `CLERK_TEST_CODE`, taps `verify.customSignUp.verifyCode`, and waits for `signedIn` true with `sessionStatus` `active` and a non-null `userId`. Screenshots `custom-signup-code` and `custom-signed-up`.
- **Proof.** `request-code` asserts `signUpStatus` `missing_requirements` with `signedIn` false. `complete` asserts an active session with a non-null `userId`, which `states.jsonl` records.

## Gotchas

- Type only an address that `host.newEmail` returned, so the run records the identity it created.
- The request-code spec leaves an unfinished sign-up. Clerk expires it, and it creates no user.
- The fixture's password field has the one-time-code content type (`textContentType='oneTimeCode'` in `screens/CustomSignUp.tsx`). Without it, iOS covers the keyboard with a `Use Strong Password?` sheet as soon as the field has focus. With it, a spec that only focuses the field sees the keyboard and no sheet. A typed password has not been observed.
Loading
Loading