Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/expo-example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXPO_PUBLIC_ZERODEV_PROJECT_ID=
2 changes: 2 additions & 0 deletions apps/expo-example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useConnection, WagmiProvider } from 'wagmi'
import { ChainSwitcher } from './components/ChainSwitcher'
import { ConnectionStatusBar } from './components/ConnectionStatusBar'
import { OTPAuth } from './components/OTPAuth'
import { PasskeyAuth } from './components/PasskeyAuth'
import { SendTransaction } from './components/SendTransaction'
import { wagmiConfig } from './wagmi.config'

Expand All @@ -20,6 +21,7 @@ function Content() {
<ConnectionStatusBar />
<ChainSwitcher />
<ScrollView contentContainerStyle={styles.content}>
<PasskeyAuth />
<OTPAuth />
{status === 'connected' && <SendTransaction />}
</ScrollView>
Expand Down
267 changes: 267 additions & 0 deletions apps/expo-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# @zerodev/expo-example

Internal Expo / React Native harness used to exercise
[`@zerodev/wallet-core`](../../packages/core) and
[`@zerodev/wallet-react`](../../packages/react) while developing the SDK.
The app is workspace-linked to the packages via `workspace:^`, so changes
rebuilt in the packages are picked up locally.

Primary testing target is the **Android Emulator**. Web is supported for
non-native code paths. iOS is out of scope — passkey and Universal Link
testing on iOS requires an Apple Developer Account.

## Prerequisites

- Node (recent LTS)
- pnpm 10.17.1+ (matches the repo's `packageManager` pin)
- Android Studio + JDK 17 + Android SDK (API 35 recommended) — follow
Expo's [local Android Emulator setup guide][expo-env-setup] for the
exact tooling steps
- A running Android Emulator (`adb devices` to verify)

## Environment setup

Copy the template and fill in your project ID:

```
cp .env.example .env
```

Set `EXPO_PUBLIC_ZERODEV_PROJECT_ID` to a project ID from the
[ZeroDev dashboard](https://dashboard.zerodev.app). The `EXPO_PUBLIC_`
prefix exposes the value to the JS runtime (this is a project ID, not a
secret).

## Install and build SDK packages

From the repo root:

```
pnpm install
pnpm build
```

`pnpm install` links `@zerodev/wallet-core` and `@zerodev/wallet-react`
into the app. `pnpm build` compiles those packages — required, since the
app imports from their `dist/` output, not source. During active SDK work,
rebuild only the package you changed and restart Metro with a cleared
cache:

```
pnpm -F @zerodev/wallet-react build
cd apps/expo-example && pnpm start --clear
```

### A note on `metro.config.js`

In a pnpm workspace, packages are hoisted into `node_modules/.pnpm` and
every dependency with peer deps gets a separate entry per consumer
(different peer-dep hashes). Without intervention, Metro resolves
`react` (and other shared libs) from two places: one copy for the app,
another copy for each workspace package that lists it as a peer dep.
Two copies of React in the same bundle means two sets of contexts, which
breaks hooks with errors like *"Invalid hook call"* or *"hooks called
outside of a provider"*.

`metro.config.js` installs a custom `resolveRequest` that funnels certain
imports to the app's single copy. It has two tiers:

- **`globalSingletons`** — `react`, `react-dom`, `react-native`, `tslib`.
Always redirected to the app's copy, regardless of who's importing
them. Safe because these are leaf packages.
- **`workspaceSingletons`** — `wagmi`, `@wagmi/core`,
`@tanstack/react-query`. Redirected to the app's copy **only** when
imported from a workspace package (`packages/*`). App-originating
imports are left alone.

#### When you might need to edit this file

If you add a new dependency to `packages/core` or `packages/react` that
itself stores React context, uses hooks, or is itself a peer-dep'd
shared library — and you see *"Invalid hook call"*, duplicate-provider
errors, or `instanceof` checks failing across module boundaries — the
new dep likely needs to be added to one of the two sets:

- Add to `globalSingletons` if it's a small leaf package and both the
app and workspace packages depend on it directly.
- Add to `workspaceSingletons` if the app may legitimately want its own
copy but workspace packages should defer to the app's.

After editing, restart Metro with a cleared cache:

```
cd apps/expo-example && pnpm start --clear
```

## Development build (Android)

Expo Go will **not** work for this app because it depends on native
modules that aren't in the Expo Go bundle:

- `@turnkey/react-native-passkey-stamper`
- `expo-secure-store`
- `react-native-get-random-values`

You need your own development build. For background on what a development
build is and when it's required, see Expo's
[Development builds introduction][expo-dev-build-intro].

The `android/` directory is **not committed** — it's generated by
`expo prebuild`, which `expo run:android` runs automatically on first
launch.

### First-time run (or after native dep changes)

```
cd apps/expo-example
npx expo run:android
```

This runs prebuild → Gradle build → installs the dev client APK on the
connected emulator → starts Metro.

### Subsequent runs (JS-only changes)

```
pnpm android # or: pnpm start, then press 'a'
```

Starts Metro; the already-installed dev client connects to it.

### Web (limited)

```
cd apps/expo-example && pnpm web
```

On web, the SDK packages resolve to their default web code paths (the
same ones a browser app would use) — not the React Native code. So a web
run exercises a different branch of the SDK than an Android run. Useful
for quickly sanity-checking browser behavior, but native-module features
(secure store, passkeys, etc.) won't work here — use the Android build
for those.

### Viewing native logs (`adb logcat`)

Metro shows JavaScript logs. Native-side issues — native module errors
(passkey stamper, secure store), OAuth intent-filter problems, Gradle
runtime crashes — only surface in Android's logcat.

First, find the installed package name (it depends on what was set
during `expo prebuild`; defaults to `com.anonymous.expoexample`):

```
adb shell pm list packages | grep expoexample
```

The output is prefixed with `package:` — strip that prefix before using
the name below.

**Make sure the app is running on the emulator first** (`pidof` only
finds PIDs of running processes — if the app isn't open, the command
substitution returns empty and logcat fails with
`pid '' out of range`). Then tail logs for just this app:

```
adb logcat --pid=$(adb shell pidof -s <package-name-from-above>)
```

A few other useful variants (no running app needed). Quote the filter
args so zsh doesn't try to glob-expand the `*`:

```
adb logcat "*:E" # errors only, all processes
adb logcat -c # clear the buffer first
adb logcat ReactNative:V ReactNativeJS:V "*:S" # RN-only output
```

Tip: run `adb logcat -c` right before reproducing a bug to keep the
output scoped to the repro window.

## Running with passkeys

Android passkeys work out of the box with `pnpm android` — this repo
ships a committed debug keystore, a config plugin that wires it into
Gradle's signing config, and a Vercel-hosted `assetlinks.json` pinned
to its SHA-256.

For how the binding works, how to (re)deploy the assetlinks file, how
to produce a standalone APK with EAS local, or how to add a fingerprint
when you sign a build with a different keystore, see
[`docs/passkeys.md`](./docs/passkeys.md).

## Project structure

```
apps/expo-example/
├── App.tsx # WagmiProvider + QueryClientProvider root
├── index.ts # Expo registerRootComponent entry
├── wagmi.config.native.ts # Android: native stampers + storage
├── wagmi.config.web.ts # web fallback
├── components/
│ ├── ConnectionStatusBar.tsx # address display, copy, disconnect
│ ├── ChainSwitcher.tsx # Sepolia / Arbitrum Sepolia
│ ├── GoogleAuth.tsx # OAuth flow
│ ├── OTPAuth.tsx # email OTP flow
│ └── SendTransaction.tsx # balance + send form
├── lib/
│ ├── secureStoreStamper.ts # P256 key pair in expo-secure-store
│ ├── passkeyStamper.ts # Turnkey native passkey stamper wrapper
│ └── asyncSessionStorage.ts # AsyncStorage adapter for SDK session
├── metro.config.js # monorepo singleton resolver
├── babel.config.js # babel-preset-expo + import.meta
├── app.json # scheme: zerodev-example; android.package
├── eas.json # EAS local build profile (preview)
├── credentials.json # points EAS at the committed keystore
├── credentials/
│ └── debug.keystore # shared debug keystore (committed)
├── plugins/
│ └── withDebugKeystore.js # rewrites gradle to sign with that keystore
├── assetlinks/
│ ├── public/.well-known/
│ │ └── assetlinks.json # Digital Asset Links for passkeys
│ └── vercel.json # Vercel config for the assetlinks site
├── docs/
│ └── passkeys.md # deeper dive: assetlinks, APK builds, signing
└── android/ # generated by expo prebuild (gitignored)
```

## Troubleshooting

- **Stale Metro cache** (duplicate React / wagmi, "hooks called outside
provider"):
```
cd apps/expo-example && pnpm start --clear
```
- **Workspace package changes not reflected:** the app imports from
`dist/`, so unbuilt source won't propagate. Rebuild the changed
package, then restart Metro with a cleared cache:
```
pnpm -F @zerodev/wallet-react build
cd apps/expo-example && pnpm start --clear
```
- **Gradle / prebuild out of sync after changing native deps:** delete
`android/` and re-run the first-time build:
```
cd apps/expo-example
rm -rf android
npx expo run:android
```
- **OAuth redirect does nothing:** the deep-link scheme is
`zerodev-example` (see `app.json`). After changing `scheme`, uninstall
the dev client from the emulator so Android picks up the new intent
filter, then rebuild. Find the installed package and uninstall it via
`adb`:
```
adb shell pm list packages | grep expoexample
adb uninstall <package-name-from-above>
cd apps/expo-example && npx expo run:android
```
(Alternatively: long-press the app icon on the emulator home screen →
*App info* → *Uninstall*.)
- **Passkey register fails:** usually a fingerprint mismatch between
the installed APK and the deployed `assetlinks.json`. See
[`docs/passkeys.md`](./docs/passkeys.md) for diagnosis and fixes.

[expo-env-setup]: https://docs.expo.dev/get-started/set-up-your-environment/?mode=development-build&buildEnv=local&platform=android&device=simulated
[expo-dev-build-intro]: https://docs.expo.dev/develop/development-builds/introduction/
8 changes: 6 additions & 2 deletions apps/expo-example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"scheme": "zerodev-example",
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "com.zerodev.expoexample"
},
"android": {
"package": "com.zerodev.expoexample",
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
Expand All @@ -25,6 +28,7 @@
},
"web": {
"favicon": "./assets/favicon.png"
}
},
"plugins": ["./plugins/withDebugKeystore"]
}
}
1 change: 1 addition & 0 deletions apps/expo-example/assetlinks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
15 changes: 15 additions & 0 deletions apps/expo-example/assetlinks/public/.well-known/assetlinks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.zerodev.expoexample",
"sha256_cert_fingerprints": [
"11:F4:8E:00:20:B4:FB:77:BF:6C:86:A2:62:8F:F5:B4:69:6F:4C:68:ED:C0:90:79:56:93:6C:8E:50:4A:6E:B0"
]
}
}
]
11 changes: 11 additions & 0 deletions apps/expo-example/assetlinks/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"cleanUrls": false,
"trailingSlash": false,
"outputDirectory": "public",
"headers": [
{
"source": "/.well-known/assetlinks.json",
"headers": [{ "key": "Content-Type", "value": "application/json" }]
}
]
}
Loading