Checking Subscription Status
Read the current person's entitlements and subscription state to decide what to unlock.
Voidhash exposes everything it knows about the current user as a single person snapshot: their
identity, entitlement grants, current subscription, and purchase history. You read it reactively
with useCurrentPerson() or imperatively with client.getCurrentPerson().
Reading the Person Reactively
useCurrentPerson() returns { data, error, isLoading, refetch }. It fetches the snapshot once the
Provider has initialized and re-renders whenever the client publishes an updated snapshot:
import { voidhash } from "utils/voidhash/client";
export default function AccountScreen() {
const { data: person, error, isLoading, refetch } = voidhash.useCurrentPerson();
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage error={error} onRetry={refetch} />;
}
return (
<View>
<Text>Person ID: {person.personId}</Text>
<Text>Subscription: {person.subscriptions?.current?.status ?? "none"}</Text>
</View>
);
}data can be empty
While the snapshot is loading — and for a brand-new anonymous user that has no server-side person
yet — data is an empty object {}, not null. Always use optional chaining
(person.subscriptions?.current) when reading nested fields.
Reading the Person Imperatively
Outside of React — in event handlers, background logic, or your own state layer — use
client.getCurrentPerson(forceFetch?). It resolves to the snapshot, or null for a brand-new
person that has not been created on the server yet:
import { voidhash } from "utils/voidhash/client";
const person = await voidhash.client.getCurrentPerson();
// Bypass the cache and always hit the server:
const fresh = await voidhash.client.getCurrentPerson(true);If the call fails, it throws a VoidhashError with a FAILED_TO_GET_CURRENT_PERSON code — see
Error Handling.
The Person Snapshot
The snapshot has this shape:
type SdkPersonSnapshot = {
personId: string;
distinctId: string;
email: string | null;
name: string | null;
entitlements: {
grants: Array<{
perkId: string;
status: "active" | "expired";
source: "subscription" | "purchase" | "manual";
sourceId: string | null;
expiresAt: Date | null;
}>;
};
subscriptions: {
current: {
status: "none" | "active" | "canceled" | "past_due" | "trialing";
expiresAt: Date | null;
productId: string | null;
subscriptionId: string | null;
} | null;
history: Array<{
subscriptionId: string;
productId: string | null;
status: "active" | "canceled" | "expired" | "trialing" | "past_due";
startsAt: Date;
expiresAt: Date | null;
canceledAt: Date | null;
isTrial: boolean;
}>;
};
purchases: {
history: Array<{
purchaseId: string;
productId: string | null;
type: "one_time" | "subscription";
providerKey: string;
createdAt: Date;
}>;
};
snapshotContext: {
mode: "persisted" | "temporary_pending_transfer";
};
};Entitlements
entitlements.grants is the list of perks the person holds. Each grant names the perk from your
schema (perkId), whether it is currently active or expired, where it came from (source:
a subscription, a one-time purchase, or a manual grant from the dashboard), and when it expires
(expiresAt is null for grants that never expire). This is the field to gate features on —
it already accounts for expiry and for perks granted outside of subscriptions.
Subscriptions
subscriptions.current describes the person's current subscription, or is null/status: "none"
when they have never subscribed. Use it for account UI — showing the renewal date, prompting a
past_due user to fix their payment method, or messaging a trialing user. subscriptions.history
lists past and present subscriptions, including canceled and expired ones.
Purchases
purchases.history lists completed purchases (both one-time purchases and subscription
transactions) with the product, the payment provider (providerKey), and the purchase date.
Snapshot context
snapshotContext is mostly internal. mode is "temporary_pending_transfer" while an
identify migration is merging two persons; once the migration
completes it returns to "persisted".
Gating Access to a Feature
Check for an active grant of the perk you defined in your schema. A small wrapper hook keeps call sites clean:
import { voidhash } from "utils/voidhash/client";
export function useHasPerk(perkId: string) {
const { data: person, error, isLoading, refetch } = voidhash.useCurrentPerson();
const hasPerk =
person.entitlements?.grants.some(
(grant) => grant.perkId === perkId && grant.status === "active",
) ?? false;
return { hasPerk, error, isLoading, refetch };
}import { useHasPerk } from "hooks/use-has-perk";
export default function PremiumFeature() {
const { hasPerk, error, isLoading } = useHasPerk("pro");
if (isLoading) {
return <LoadingSpinner />;
}
// If the status check failed, fall back to the paywall instead of
// guessing — the user can always restore or retry from there.
if (error || !hasPerk) {
return <PaywallScreen />;
}
return <PremiumContent />;
}Gate on grants, not on subscription status
subscriptions.current.status === "active" looks tempting, but it misses perks granted by
one-time purchases and manual grants, and it does not tell you which features the subscription
unlocks. Grants are the entitlement source of truth; use subscription status for account UI only.
Caching
Person snapshots are cached on-device per distinct id, so subscription checks are instant and work
through flaky networks. getCurrentPerson() (and the hook) use a fetch-while-stale policy:
- A cached snapshot fresher than 5 minutes is returned directly, with no network request.
- A snapshot older than 5 minutes is considered stale and is re-fetched from the server.
- Cache entries expire entirely after 2 days; an expired entry is never served.
getCurrentPerson(true)skips the cache and always fetches from the server.
You rarely need forceFetch because the SDK keeps the cache in sync at the moments that matter:
- On init the person is prefetched in the background.
- After a successful purchase or restore the SDK re-fetches the snapshot from the server, so
new entitlements are reflected immediately — your next
getCurrentPerson()orrefetch()returns them without a stale window. - After
identify()the cache key changes with the distinct id, so the previous identity's snapshot is never served for the new one. Callrefetch()(orgetCurrentPerson()) after identifying to load the new person's data.
Next Steps
- Identify users so entitlements follow them across devices.
- Make purchases that grant the perks you gate on.
- Generate typed slugs for your products and perks.
- Handle errors thrown by the client methods used here.