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.

RequirementVersion
PlatformsiOS and Android.
Expo SDKVerified on 55. Developed against 54. Bare React Native apps do not need Expo.
React NativeVerified on 0.83. Developed against 0.81.
react-native-nitro-modulesPeer ^0.37.1. Use a 0.37.x release.
effectPeer 4.0.0-rc.115. Install this exact version.
Google Play Billing Library9. The SDK adds it on Android, so the app's own billing library must support Billing 8 or 9.
react-native-iap, if used16.6.2 or later. Earlier releases require an older Nitro minor or an older Billing Library.

react and react-native are also peer dependencies. expo is an optional peer that only the config plugin uses.

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. On iOS, the app's Podfile must reference the VoidhashCore pod. The quickstart shows the config plugin for Expo apps and the Podfile line for bare React Native apps.

Data collection

Account for the data the SDK sends to Voidhash when you complete the App Store privacy details and the Google Play Data safety form.

DataWhen the SDK collects itUsed for
Device IDAlways. The SDK creates an anonymous ID for each app install.App functionality, analytics
User IDWhen you call identify() with your own user ID.App functionality, analytics
Email address and nameOnly when you pass them to identify() or set them as person attributes.App functionality
Purchase historyAlways. The SDK reports the store transactions it observes.App functionality, analytics
Product interactionBy default. Captured events, app lifecycle events, and screen views.Analytics

Requests also carry device and app details such as the device model, OS version, app version, and locale. The data is linked to the customer's identity in Voidhash. The SDK does not read advertising identifiers and does not share data with other companies for tracking.

Create the client

Create one client and keep it for the lifetime of the app.

src/lib/voidhash.ts
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.

OptionDefaultDescription
schemeFirst URL scheme of the appDeep-link scheme for purchase callbacks. Read natively when omitted.
distinctIdPersisted or new anonymous IDSeeds the initial customer identity. Usually omit it and call identify(). A blank value is ignored.
debugfalseEnables additional SDK diagnostics.
devfalseReserved for SDK-started test purchases.
enabledtrueSet it to false to ship the SDK fully inert. Fixed at construction.
readOnlytrueForced on while commerce features are unavailable.
baseUrlhttps://api.voidhash.comOverrides the API origin for self-hosted deployments.
ingestUrlSame origin as baseUrlOverrides only the analytics origin.

baseUrl and ingestUrl must be absolute http:// or https:// URLs, and a blank value counts as not set. In a development build, any other value throws an INVALID_ARGUMENT error from createVoidhashClient. A release build falls back to the default origin and reports an INVALID_CONFIGURATION diagnostic through onDiagnostic, so a mistyped environment variable cannot silently stop every request.

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. A report made during launch waits for the client to initialize. 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> above the rest of your app. It initializes identity, the schema, person state, the store adapters, and the transaction observer. While it initializes, hooks report isLoading: true.

<voidhash.Provider>
  <App />
</voidhash.Provider>

useVoidhash() reads the provider's state. It returns the fields below.

FieldTypeDescription
status"initializing" | "ready" | "failed" | "disabled"The current lifecycle state. disabled is terminal.
initErrorError | nullThe initialization error. Set only while status is failed.
retryInit() => voidRe-runs init(). Does nothing unless status is failed.
isInitializedbooleanAn alias for status === "ready".
clientVoidhashClientThe underlying imperative client.

A cold start with no network still initializes, from the state saved on the device. Initialization fails when the app binary does not contain the SDK's native modules, for example after an over-the-air update to a build made with an older SDK version. 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} />;
}

On iOS, an app can be launched in the background after the device restarts but before the user unlocks it, for example by a VoIP push or a location event. The SDK cannot read its saved state until that first unlock, so the provider stays initializing until then. Events you capture meanwhile are kept and sent later, and the user keeps their existing identity and access once the device is unlocked.

Apps with several React roots, such as react-native-navigation screens or brownfield views, mount a provider in each root. The providers share one client: it starts with the first provider and stops when the last one unmounts.

Hooks

The SDK exposes five hooks. The data hooks report isLoading: true while the provider initializes and until their first read answers, and false for a disabled client. Their results are plain values: error is null when there is none. After identify() or reset(), mounted hooks read again for the new person.

HookDescription
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 configured in Studio and read hasAccess. Grants carry that slug as perkSlug; perkId is the perk's internal ID.

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 are cache-first: a snapshot younger than five minutes is served from the cache without a request, and an older one is served 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. Queued analytics events and purchase reports are kept in their own files in the app's private storage. The cached copy survives up to two days, which is why access checks fail open offline. The SDK refreshes the snapshot on its own at launch, on foreground, and after purchases, restores, and identity changes, and the hooks re-render when a refresh lands.

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 access = await voidhash.client.hasPerk("premium");
const hasAccess = access.isOk() && access.value.hasAccess;
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 generate

The 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.