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.
React NativeVerified on 0.83. Developed against 0.81.
react-native-nitro-modulesPeer ^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.

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().
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.

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.

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.

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.

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