React Native SDK
Client configuration, hooks, and runtime behavior for @voidhash/react-native.
@voidhash/react-native supports Expo development builds and bare React Native apps on iOS and
Android.
Initial release is observer-only
SDK-started purchases and hosted paywalls are temporarily unavailable. Store transactions are still observed and submitted to Voidhash for revenue analytics, but the SDK never finishes or acknowledges them.
Compatibility
| Requirement | Version |
|---|---|
| Platforms | iOS and Android |
| Expo SDK | Verified on 55; developed against 54 |
| React Native | Verified on 0.83; developed against 0.81 |
react-native-nitro-modules | Peer ^0.35.5 — use a 0.35.x release |
@react-native-async-storage/async-storage | Peer ^1.24.0 || ^2.0.0 |
effect, expo, expo-constants, expo-linking, react, and react-native are also peer
dependencies and must be installed in the app.
Nitro versions must match
Every Nitro-based module in the app resolves against one shared native runtime. Mixing Nitro versions across modules produces native build or load failures that do not point back at Voidhash. Check every dependency that ships Nitro specs before upgrading one of them.
Because the package ships native code, changing it requires a new development build — Expo Go cannot load it.
Create the client
Create one client for the lifetime of the app:
import { createVoidhashClient } from "@voidhash/react-native";
export const voidhash = createVoidhashClient("vh_pk_...");The publishable key is safe to include in the app. Never ship vh_sk_... secret keys.
createVoidhashClient takes the publishable key and an options object. There is no schema argument:
the schema lives on the server and is fetched when the provider mounts.
Client options
| Option | Default | Use |
|---|---|---|
scheme | Expo app scheme | Reserved for paywalls; not required in the initial release. |
distinctId | Persisted or new anonymous ID | Seed the initial customer identity. Usually omit it and call identify(). |
debug | false | Enable additional SDK diagnostics. |
dev | false | Reserved for SDK-started test purchases. |
enabled | true | Set false to ship the SDK fully inert. Fixed at construction. |
readOnly | true | Forced on while commerce features are unavailable. |
baseUrl | https://api.voidhash.com | Override the API origin for self-hosted deployments. |
ingestUrl | Same origin as baseUrl | Override only the analytics origin. |
Disabled clients
enabled: false returns a client that never connects to the native store, never opens a network
connection, and never registers a listener. init() and every side-effect method no-op, reads
answer with their empty shape, and the scheme requirement is waived.
Mount the provider unconditionally either way. Every hook still mounts on a disabled client, so hook order never changes between a flagged-off and a flagged-on build. Enabling the SDK later means creating a new client, which is cheap because a disabled one never built its runtime.
Observer mode
The initial release always reports client.isReadOnly === true. Passing readOnly: false or
calling client.setReadOnly(false) cannot transfer store ownership to Voidhash yet.
Observed and restored transactions are submitted to Voidhash, but are never finished or
acknowledged. purchase() returns READ_ONLY_PURCHASE_NOT_ALLOWED, while reads,
restorePurchases(), identity, feature flags, and analytics keep working.
Provider
Mount <voidhash.Provider> once. It initializes identity, schema, person state, store adapters, and
the transaction observer. Hooks wait until initialization completes.
<voidhash.Provider>
<App />
</voidhash.Provider>useVoidhash() reads the provider's state:
| Field | Type | Notes |
|---|---|---|
status | "initializing" | "ready" | "failed" | "disabled" | disabled is terminal. |
initError | Error | null | Set only while status is "failed". |
retryInit | () => void | Re-runs init(). No-op unless status is failed. |
isInitialized | boolean | Alias for status === "ready". |
client | VoidhashClient | The underlying imperative client. |
Initialization can fail — a cold start with no network, for example. Handle it rather than leaving the app in a permanent loading state:
const { status, initError, retryInit } = voidhash.useVoidhash();
if (status === "failed") {
return <RetryScreen error={initError} onRetry={retryInit} />;
}Hooks
| Hook | Use |
|---|---|
useProducts() | Read store-backed product metadata. |
useHasPerk(slug) | Check whether the current person holds an active perk grant. |
useCurrentPerson() | Read entitlements, subscription state, and purchase history. |
useFeatureFlags(keys?) | Evaluate flags and variants for the current person. |
useVoidhash() | Read provider state and the underlying client. |
useHasPerk
The fastest way to gate a feature. See Check access for offline behavior:
const { hasAccess, grant, isLoading, isStale, error, refetch } = voidhash.useHasPerk("premium");useCurrentPerson
Returns { data, error, isLoading, refetch }. data is the full person snapshot, or null until
the first snapshot loads or while the client is disabled:
type Person = {
personId: string;
distinctId: string;
name: string | null;
email: string | null;
entitlements: { grants: Grant[] };
subscriptions: {
current: {
productId: string | null;
status: string;
subscriptionId: string | null;
expiresAt: Date | null;
} | null;
history: SubscriptionEntry[];
};
purchases: { history: PurchaseEntry[] };
snapshotContext: {
mode: "persisted" | "temporary_pending_transfer";
includedPersonIds: string[];
migrationJobId: string | null;
};
};Reads use a stale-while-revalidate cache: snapshots younger than five minutes are served from cache while a fresh copy loads in the background; the cached copy survives up to two days so access checks fail open offline. The SDK refreshes the snapshot after purchases, restores, and identity changes.
Imperative client
The voidhash.client object exposes the same core workflows outside React:
await voidhash.client.identify(user.id);
const products = await voidhash.client.getProducts();
const person = await voidhash.client.getCurrentPerson();
const { hasAccess } = await voidhash.client.hasPerk("premium");
await voidhash.client.restorePurchases();
voidhash.client.capture("screen_viewed", { screen: "home" });getCurrentPerson({ forceFetch: true }) skips the cache. identify() takes optional
{ email, name } attributes.
Generated slug types
Run this after changing products or perks in Studio:
npx voidhash-cli types generateThe generated declaration augments the SDK's slug types. Without it, the SDK still works but slugs
fall back to string.
Error handling
Hooks expose error; client methods return a better-result Result and never reject. Keep
transport failures separate from negative product state:
const { data: person, error, isLoading, refetch } = voidhash.useCurrentPerson();
if (isLoading) return <LoadingScreen />;
if (error) return <RetryScreen onRetry={refetch} />;
return <Account person={person} />;Every error carries a stable code, such as FAILED_TO_GET_CURRENT_PERSON. Match on
result.error.code when recovery differs, and report the full error for everything else. The
complete code list with recovery guidance lives on the Errors page.