React Native SDK
Configure @voidhash/react-native and look up its provider, hooks, and client.
The React Native SDK, @voidhash/react-native, adds entitlements, feature flags, analytics, and
observer-mode transaction reporting to Expo development builds and bare React Native apps on iOS
and Android. Use this page to create the client, set its options, and find the provider fields,
hooks, and client methods you need. Each feature also has its own guide, linked from the section
that introduces it.
The 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
The SDK has the following requirements.
| 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. |
effect, expo, react, and react-native are also peer
dependencies. Install all of them in the app.
Nitro versions must match across modules
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.
The package ships native code, so changing it requires a new development build. Expo Go cannot load it.
Create the client
Create one client and keep it 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, because the schema lives on the server and the SDK fetches it when the provider mounts.
Client options
Pass an options object as the second argument to createVoidhashClient to change the defaults.
Each option is described below.
| Option | Default | Description |
|---|---|---|
scheme | First URL scheme of the app | Deep-link scheme for purchase callbacks. Read natively when omitted. |
distinctId | Persisted or new anonymous ID | Seeds the initial customer identity. Usually omit it and call identify(). |
debug | false | Enables additional SDK diagnostics. |
dev | false | Reserved for SDK-started test purchases. |
enabled | true | Set it to false to ship the SDK fully inert. Fixed at construction. |
readOnly | true | Forced on while commerce features are unavailable. |
baseUrl | https://api.voidhash.com | Overrides the API origin for self-hosted deployments. |
ingestUrl | Same origin as baseUrl | Overrides only the analytics origin. |
Disabled clients
With enabled: false, every method is inert. The client never connects to the native store, never
opens a network connection, and never registers a listener. init() and every side-effect method
do nothing, 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. To enable the SDK later, create a new client. That is cheap, because a disabled client never built its runtime.
Observer mode
The initial release always uses observer mode, so client.isReadOnly is always 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 the SDK never finishes or
acknowledges them. purchase() returns READ_ONLY_PURCHASE_NOT_ALLOWED. Reads,
restorePurchases(), identity, feature flags, and analytics keep working.
After a successful host purchase, call reportTransaction(...) with its store transaction values.
A valid report is persisted before it resolves and delivered in the background, so a network or
service outage delays reporting without making the purchase flow return an error. Use
syncPurchases() for callbacks without transaction values and for restore recovery. Store observers
and scans cannot recover a consumable already finished or consumed by the host.
See Report and restore purchases for callback examples, error
handling, and scan limits.
Provider
Mount <voidhash.Provider> once, above the rest of your app. It initializes identity, the schema,
person state, the store adapters, and the transaction observer. Hooks wait until initialization
completes.
<voidhash.Provider>
<App />
</voidhash.Provider>useVoidhash() reads the provider's state. It returns the fields below.
| Field | Type | Description |
|---|---|---|
status | "initializing" | "ready" | "failed" | "disabled" | The current lifecycle state. disabled is terminal. |
initError | Error | null | The initialization error. Set only while status is failed. |
retryInit | () => void | Re-runs init(). Does nothing unless status is failed. |
isInitialized | boolean | An alias for status === "ready". |
client | VoidhashClient | The underlying imperative client. |
Initialization can fail, for example on a cold start with no network. Handle that case 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
The SDK exposes five hooks.
| Hook | Description |
|---|---|
useProducts() | Reads store-backed product metadata. |
useHasPerk(slug) | Checks whether the current person holds an active grant for a perk. |
useCurrentPerson() | Reads entitlements, subscription state, and purchase history. |
useFeatureFlags(keys?) | Evaluates flags and variants for the current person. |
useVoidhash() | Reads provider state and the underlying client. |
useHasPerk
useHasPerk is the fastest way to gate a feature. Pass the perk slug and read hasAccess.
const { hasAccess, grant, isLoading, isStale, error, refetch } = voidhash.useHasPerk("premium");See Check access for how the hook behaves offline.
useCurrentPerson
useCurrentPerson returns { data, error, isLoading, refetch }. data is the full person
snapshot. It is null until the first snapshot loads, and stays null while the client is
disabled. The snapshot has the following shape.
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 the
cache while a fresh copy loads in the background. The cache lives in the same native store the
Swift and Kotlin SDKs use (UserDefaults on iOS, SharedPreferences on Android), so no extra
storage package is needed. The cached copy survives up to two days, which is why access checks fail open offline.
The SDK refreshes the snapshot on its own after purchases, restores, and identity changes.
Imperative client
Outside React, voidhash.client exposes the same core workflows as the hooks.
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" });Pass getCurrentPerson({ forceFetch: true }) to bypass the cache. identify() also accepts
optional { email, name } attributes.
Generated slug types
Run the CLI after you change products or perks in Studio to regenerate the slug types.
npx voidhash-cli types generateThe generated declaration augments the SDK's slug types, so product and perk slugs are checked at
compile time. Without it, the SDK still works, but slugs fall back to string.
Error handling
Hooks expose an error field. Client methods return a Result value from the better-result
library, so they never reject. Keep transport failures separate from negative product state, such as
a person who simply has no active grant.
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 by error, and report the full error for everything else.
The complete code list with recovery guidance lives on the Errors page.