Browse documentation

SubKit owns the complete access decision. Your app names the entitlement it needs — for example pro — and reads one discriminated union. Do not combine raw CustomerInfo.entitlements and CustomerInfo.deviceAccess fields in app code.

This is the canonical guide for an access check, entitlement check, feature gate, Pro gate, or device-blocked recovery flow.

The simple gate

import { useSubKitAccess } from '@piparotech/subkit-expo'

const PRO = 'pro'

export function ProGate() {
  const access = useSubKitAccess(PRO)

  if (access.state === 'loading') return <LoadingState />
  if (access.state !== 'granted') return <Paywall onPurchaseFinished={access.refresh} />
  return <PaidFeatures />
}

state === 'granted' is the only detailed unlock signal. It already includes:

  • the requested entitlement being commercially active;
  • the currently authorized installation/device context;
  • offline expiry and cache policy;
  • provider-verified SubKit state.

For a fail-closed Boolean gate where no recovery UI is needed:

import { useSubKitHasAccess } from '@piparotech/subkit-expo'

export function ProFeature() {
  const hasPro = useSubKitHasAccess('pro')
  return hasPro ? <PaidFeatures /> : <Paywall />
}

Access states

useSubKitAccess() returns one impossible-state-safe union:

State Meaning Typical UI
granted Requested entitlement is active and usable on this installation Render the protected feature
missing CustomerInfo has no entry for the requested entitlement Paywall or explain unavailable plan
inactive Requested entitlement exists but is not commercially active Renewal or win-back UI
device_blocked Requested entitlement is active, but this installation needs recovery Device-management recovery
loading No access decision is available yet Loading state
offline_unavailable Network unavailable and no bounded cached decision exists Offline retry state
error A non-network refresh/configuration failure prevents a decision Error and retry
unconfigured configureSubKit() has not installed the singleton Treat as integration error

State-specific data only exists where valid. For example, reason exists only on device_blocked; a granted decision cannot carry a block reason.

Complete recovery UI

import { useSubKitAccess } from '@piparotech/subkit-expo'

export function ProAccessBoundary() {
  const access = useSubKitAccess('pro')

  switch (access.state) {
    case 'granted':
      return <PaidFeatures />
    case 'device_blocked':
      return <DeviceRecovery reason={access.reason} />
    case 'missing':
    case 'inactive':
      return <Paywall onPurchaseFinished={access.refresh} />
    case 'loading':
      return <LoadingState />
    case 'offline_unavailable':
      return <OfflineNotice />
    case 'error':
      return <PaywallError onRetry={access.refresh} />
    case 'unconfigured':
      return <ConfigurationError />
  }
}

Evidence and commercial details

Resolved decisions carry evidence:

const access = useSubKitAccess('pro')

if (access.state === 'granted') {
  access.entitlement.active // literal true
  access.entitlement.status // active, trialing, grace_period, ...
  access.entitlement.expiresAt
  access.evidence.checkedAt
  access.evidence.freshness // fresh, stale, syncing, offline, error
}

entitlement.active describes commercial state. access.state === 'granted' describes effective feature access. Apps should gate on the latter.

Imperative reads

Outside React, ask the configured client for the same canonical decision:

import { client } from '@piparotech/subkit-expo'

const access = await client.getAccess('pro')
if (access.state === 'granted') {
  unlockPaidAccess()
}

For a Boolean:

const hasPro = await client.hasAccess('pro')

For synchronous app-store integration or external-state bridges:

import { getSubKitAccessSnapshot, subscribeSubKitAccess } from '@piparotech/subkit-expo'

const current = getSubKitAccessSnapshot('pro')
const unsubscribe = subscribeSubKitAccess('pro', (access) => {
  renderAccessState(access)
})

Refresh behavior

The hook refreshes on mount when the shared snapshot is older than refreshIfOlderThanMs (default 60 seconds). Customize it when necessary:

useSubKitAccess('pro', {
  enabled: true,
  refreshOnMount: true,
  refreshIfOlderThanMs: 60_000,
})

refresh() always resolves to the latest access union, including offline_unavailable or error; recovery UI does not need to catch transport errors merely to discover the new state.

Raw CustomerInfo is advanced diagnostics

client.getCustomerInfo() remains available for support diagnostics, ownership UI, purchase history, and advanced device management. It is not the recommended feature-gate API. Centralizing the decision in SubKit prevents apps from drifting when device policy, offline limits, or entitlement rules change.

Next

Type to search…

↑↓ navigate↵ selectEsc close