> For the complete documentation index, see [llms.txt](https://docs.alignedlayer.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.alignedlayer.com/wallet-as-a-service/framework-guides/react-native.md).

# React Native

`@aligned-waas/sdk-react-native` supplies the native pieces the core SDK needs: passkeys, encrypted storage, and a `fetch` that sends an `Origin` header. It re-exports the whole core surface, so import everything from this package.

## Install

```sh
npm install @aligned-waas/sdk-react-native
npm install react-native-passkey react-native-get-random-values expo-secure-store
```

`react-native-passkey` ships native code, so you need an Expo dev client (prebuild) or a bare React Native app. Expo Go will not work. `expo-secure-store` works in bare React Native as well, after running `npx install-expo-modules@latest` once.

## Associated domains

Passkeys are bound to a domain you control. Before anything works, `rpId` must be an HTTPS domain serving `apple-app-site-association` (iOS) and `assetlinks.json` (Android), and your app needs the matching entitlements. This is platform setup, not Aligned configuration, but it is the usual reason a first integration fails.

## Create the client

```ts
// Polyfills first, before anything constructs a client.
import '@aligned-waas/sdk-react-native/polyfills';

import {
  AlignedClient,
  reactNativePasskeyAdapter,
  secureStoreAdapter,
  createRNFetch,
} from '@aligned-waas/sdk-react-native';
import { Passkey } from 'react-native-passkey';
import * as SecureStore from 'expo-secure-store';

const RP_ID = 'wallet.example.com';

export const aligned = new AlignedClient({
  publishableKey: 'aligned_pk_test_...',
  storage: secureStoreAdapter(SecureStore),
  passkey: reactNativePasskeyAdapter({ rpId: RP_ID, passkey: Passkey }),
  fetchFn: createRNFetch({ origin: `https://${RP_ID}` }),
});
```

Sign-in, wallet creation, transactions, and key management are then identical to the [Quickstart](/wallet-as-a-service/quickstart.md) and [Keys and recovery](/wallet-as-a-service/keys-and-recovery.md).

The React hooks in `@aligned-waas/sdk-react` work here too. They depend only on `react`. Install that package alongside this one if you want them; this package does not re-export them.

## Google sign-in

The hosted flow runs in the system auth session, with `expo-web-browser` injected:

```ts
import { loginWithGoogle } from '@aligned-waas/sdk-react-native';
import * as WebBrowser from 'expo-web-browser';

const user = await loginWithGoogle(aligned, WebBrowser, { returnUrl: 'myapp://oauth' });
```

Add that deep link's origin to the publishable key's allowed origins. Custom schemes are accepted, in `scheme://host` form; `scheme:/path` has no host and is rejected. The code in the redirect is single-use, short-lived, and bound to this app instance, so a forwarded link cannot sign anyone in.

## Origin on native is not a security boundary

The backend requires an allow-listed `Origin` header on every request, and React Native's `fetch` does not send one, which is why `createRNFetch` injects it.

On native, that header proves nothing. Anyone holding the publishable key can send the same string from any HTTP client. Treat the origin allowlist as routing and rate limiting on mobile, not authentication. In a browser it is a real boundary, because browsers set `Origin` themselves and scripts cannot forge it.

## Storage

`secureStoreAdapter(SecureStore)` keeps tokens and key selection in the iOS Keychain or Android Keystore. Two settings are worth deciding before you ship:

* **Accessibility.** Expo defaults to `WHEN_UNLOCKED`, so reads return null while the device is locked, which is indistinguishable from a signed-out user. If any of your code runs outside an unlocked foreground session (background fetch, push handlers), pass `keychainOptions: { keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK }`. iOS ties accessibility to the stored item, so changing it later orphans existing ones.
* **Item size.** The adapter splits oversized values across keychain items automatically. Tune with `{ maxItemBytes }` if you need to.

## Platform notes worth knowing

* **Use a stable, opaque `userId`.** The platform keychain keys passkeys on `(rpId, userId)`. A per-call random value breaks duplicate detection and credential attribution. The signed-in user's id is the right value.
* **Offer "use the passkey on this device" before "add a device".** `keys.adoptExistingPasskey()` recovers a synced or forgotten credential in one prompt. On iOS below 17.4, duplicate detection is not enforced, so a second registration can silently replace the existing passkey.
* **Run ceremonies in the foreground.** With no foreground activity, Android's credential manager resolves to nothing; the adapter turns that into an explicit error naming the cause.
