getOfferings() resolves every configured package against the current native
store catalog. Your paywall renders translated package copy plus native store
prices from the response, and passes only the selected package’s runtime
identifier to the purchase call.
Load offerings
import { client } from '@piparotech/subkit-expo'
const offerings = await client.getOfferings()
const current = offerings.current // the default offering, or null
const all = offerings.all // every offering configured for this appTarget a specific placement when your app has more than one paywall surface:
const offerings = await client.getOfferings({ placement: 'settings_upgrade' })The response shape
Each offering contains packages; each package resolves a storeProduct from
the native store:
for (const pkg of offerings.current?.packages ?? []) {
pkg.identifier // runtime package ID — pass this to purchasePackage(...)
pkg.storeProduct?.displayPrice // localized price for the exact store product
pkg.storeProduct?.currency
pkg.storeProduct?.title
}storeProduct.displayPrice is the localized price for the exact Apple product
or Google base plan that purchasePackage(...) will buy.
Missing storeProduct means not purchasable
A package with storeProduct: null means the configured store product is not
currently purchasable on this device. Do not substitute a static catalog
price and do not offer the package for purchase:
function PaywallPackage({ pkg }: { pkg: SubKitOfferingPackage }) {
if (pkg.storeProduct == null) {
return null // hide, or show an "unavailable" state — never a fake price
}
return <PurchaseRow label={pkg.storeProduct.title} price={pkg.storeProduct.displayPrice} />
}import type { SubKitOfferingPackage } from '@piparotech/subkit-expo'The hook: useSubKitOfferings
In React components, prefer the hook over manual useEffect wiring. It loads
on mount, tracks loading/refresh state, and guards against out-of-order
responses:
import { type SubKitOfferingPackage, useSubKitOfferings } from '@piparotech/subkit-expo'
export function Paywall({ onPurchased }: { onPurchased: () => void }) {
const { current, isLoading, error, refresh } = useSubKitOfferings({
placement: 'settings_upgrade',
})
if (isLoading) return <PaywallSkeleton />
if (error != null) return <PaywallError onRetry={refresh} />
if (current == null) return <PaywallUnavailable />
const purchasable = current.packages.filter((pkg) => pkg.storeProduct != null)
if (purchasable.length === 0) return <PaywallUnavailable />
return (
<PackageList
packages={purchasable}
onSelect={(pkg: SubKitOfferingPackage) => runPurchase(pkg.identifier, onPurchased)}
/>
)
}runPurchase handles the four purchase outcomes — see
Making purchases.
Result and options
const {
current, // SubKitOffering | null — the default offering
offerings, // SubKitOfferingsResponse | null — full response (current + all)
isLoading, // first load running, no data yet
isRefreshing, // data present, reload running
error, // Error | null
refresh, // () => Promise<SubKitOfferingsResponse | null>
} = useSubKitOfferings({
placement: 'settings_upgrade', // optional placement filter
enabled: true, // set false to defer loading (e.g. until the paywall opens)
})Changing placement reloads automatically. Call refresh() after events that
can change eligibility — for example a completed purchase or identify().
Store prices come from the native store, so a paywall that stays mounted for a
long time can also refresh() on foreground.
See the hooks reference for all React APIs.
Optional: preflight sync
Before showing a high-stakes paywall you can force a purchase sync so the paywall reflects any purchase that completed outside the app:
await client.syncPurchases({ reason: 'paywall_preflight' })Rules
- Pass only
package.identifiertopurchasePackage(...). The SDK resolves the native product ID and any applicable Google base-plan/offer token from the offering. - Never hardcode store product IDs, prices, or offer tokens.
- Package identifiers, product IDs, prices, billing periods, trials, and offers are runtime catalog data — they can change without an app release.