One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Purchases

Sell in-app products and subscriptions on iOS with StoreKit 2, and deliver what people bought

One.Purchases sells in-app products on iOS through StoreKit 2. It loads products and their local prices, runs Apple’s purchase sheet, reports transactions as they arrive, and lets you finish each one after you deliver it. Android and web have no store yet: products load empty and purchases reject.

To draw Apple’s own product and subscription screens instead of your own, use iOS Store Views. Both read the same transactions.

Usage

Products come from App Store Connect for a released app, or from a StoreKit configuration file selected in your Xcode scheme while you develop. No app config or usage string is needed.

import { useEffect, useState } from 'react'
import { Button } from 'react-native'
import { One } from 'one'
const premiumId = 'com.example.app.premium'
function UpgradeButton({ onUnlocked }: { onUnlocked: () => void }) {
const [price, setPrice] = useState<string | null>(null)
useEffect(() => {
One.Purchases.getProducts([premiumId]).then(([product]) => setPrice(product?.displayPrice ?? null))
}, [])
if (!price) return null
return (
<Button
title={`Unlock for ${price}`}
onPress={async () => {
const result = await One.Purchases.purchase(premiumId)
if (result.status !== 'purchased' || !result.transaction) return
onUnlocked()
await One.Purchases.finishTransaction(result.transaction.id)
}}
/>
)
}

The button shows the price StoreKit returns, already formatted for the person’s store and currency, and finishes the transaction only after the unlock is delivered.

How it works

Products. getProducts asks the App Store for up to 100 product IDs and returns the ones it found, in the order you asked. IDs it does not know are left out rather than failing the call, so a typo shows up as a missing product.

Purchasing. purchase looks the product up again and opens Apple’s purchase sheet, which handles sign in, payment, and Face ID. It resolves purchased with a transaction, cancelled when people back out, or pending when the purchase waits on something outside the app, such as Ask to Buy approval from a parent.

Verification. StoreKit signs every transaction and checks the signature on the device. One only hands you transactions that pass that check; a purchase that fails it rejects with E_PURCHASE_UNVERIFIED. Each transaction also carries its signed form as jws, which your server can verify with Apple before it grants anything stored on the server.

Finishing. A transaction stays unfinished until you call finishTransaction. StoreKit keeps offering unfinished transactions, in getUnfinishedTransactions and to listeners at launch, so a crash between payment and delivery never loses a purchase. Finish only after you have delivered.

Updates. addTransactionListener hears transactions that arrive outside a purchase call: approved Ask to Buy requests, subscription renewals, purchases made on another device, and refunds. All listeners share one StoreKit stream, which stops when the last listener is removed. An update that fails verification arrives with status unverified, an error message, and no transaction.

Entitlements. getCurrentEntitlements returns the latest transaction for each non-consumable people own, each active subscription, and each non-renewing subscription. Refunded purchases and consumables never appear there.

Restoring. sync asks the App Store to bring this device up to date with the account’s purchase history. It may ask people to sign in, so call it only from a Restore Purchases button.

API reference

FunctionResolves
getProducts(ids)The products found, in order
purchase(id, accountToken?)The purchase result
getCurrentEntitlements()Owned and active transactions
getUnfinishedTransactions()Transactions not yet finished
finishTransaction(id)When StoreKit has finished it
addTransactionListener(onUpdate)A function that removes it, at once
sync()When the App Store sync ends

accountToken is an optional UUID that StoreKit stores with the purchase and returns in its signed form, so your server can match it to an account.

PurchaseProductTypeDescription
idstringThe product ID from App Store Connect.
stringThe product ID from App Store Connect.
typePurchaseProductTypeconsumable, nonConsumable, nonRenewable, autoRenewable, or other.
PurchaseProductTypeconsumable, nonConsumable, nonRenewable, autoRenewable, or other.
displayNamestringThe localized name.
stringThe localized name.
descriptionstringThe localized description.
stringThe localized description.
displayPricestringThe price, formatted for the store.
stringThe price, formatted for the store.
currencyCodestringThe ISO currency code, when known.
stringThe ISO currency code, when known.
PurchaseTransactionTypeDescription
idstringThe transaction ID, as a decimal string.
stringThe transaction ID, as a decimal string.
originalIdstringThe first transaction of a subscription or restore.
stringThe first transaction of a subscription or restore.
productIdstringWhich product was bought.
stringWhich product was bought.
productTypePurchaseProductTypeThe product type.
PurchaseProductTypeThe product type.
purchaseDateMsnumberWhen it was bought, in Unix milliseconds.
numberWhen it was bought, in Unix milliseconds.
expirationDateMsnumberWhen a subscription period ends.
numberWhen a subscription period ends.
revocationDateMsnumberWhen Apple refunded or revoked it.
numberWhen Apple refunded or revoked it.
jwsstringThe signed transaction for server checks.
stringThe signed transaction for server checks.

Transaction IDs are strings because StoreKit uses 64-bit numbers, which JavaScript numbers cannot always hold exactly.

CodeMeans
E_PURCHASE_INPUTBad IDs or a token that is not a UUID
E_PURCHASE_PRODUCTSThe App Store request failed
E_PURCHASE_PRODUCTThe product to buy was not found
E_PURCHASE_INACTIVEThe app was not in front
E_PURCHASE_FAILEDThe purchase failed
E_PURCHASE_UNVERIFIEDA transaction failed verification
E_PURCHASE_NOT_UNFINISHEDNo unfinished transaction has that ID
E_PURCHASE_SYNCThe restore failed or was cancelled

Platform differences

iOSAndroidWeb
StoreStoreKit 2None yetNone
ProductsFrom the App StoreEmptyEmpty
ListenerStoreKit updatesNever calledNever called
Other callsWorkRejectReject

Limits and gotchas

Examples

Deliver every transaction from one place

import { One } from 'one'
type Transaction = { id: string; productId: string; jws: string }
const delivering = new Set<string>()
async function deliver(transaction: Transaction, grant: (transaction: Transaction) => Promise<void>) {
if (delivering.has(transaction.id)) return
delivering.add(transaction.id)
try {
await grant(transaction)
await One.Purchases.finishTransaction(transaction.id)
} finally {
delivering.delete(transaction.id)
}
}
export async function startStore(grant: (transaction: Transaction) => Promise<void>) {
const remove = One.Purchases.addTransactionListener((update) => {
if (update.transaction) deliver(update.transaction, grant)
})
for (const transaction of await One.Purchases.getUnfinishedTransactions()) {
await deliver(transaction, grant)
}
return remove
}

Call startStore once at launch and route purchase results through the same deliver. grant should check jws with your server and record the transaction ID, so a transaction seen twice is granted once.

Restore purchases

import { Alert } from 'react-native'
import { One } from 'one'
async function restore(unlock: (productIds: string[]) => void) {
try {
await One.Purchases.sync()
} catch {
return
}
const owned = await One.Purchases.getCurrentEntitlements()
unlock(owned.map((transaction) => transaction.productId))
if (owned.length === 0) Alert.alert('Nothing to restore', 'This account has no purchases.')
}

sync rejects when people cancel the sign in, so the function stops quietly. Reading entitlements afterward picks up purchases made on other devices.

Edit this page on GitHub.