### Install Dependencies
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Install the necessary @biltme/iap package and its peer dependencies. Ensure all versions are consistent, especially in monorepos.
```bash
npm install @biltme/iap
npm install react react-native expo-iap @react-native-async-storage/async-storage
```
--------------------------------
### EntitlementMap Example Usage
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/types.md
An example demonstrating how to structure an EntitlementMap, showing different entitlement configurations for 'pro' and 'premium' products.
```typescript
const entitlements: EntitlementMap = {
pro: {
active: true,
status: 'active',
platform: 'IOS',
productId: 'com.example.pro.monthly',
expirationDate: 1693526400000,
isAutoRenewing: true,
},
premium: {
active: false,
status: 'expired',
platform: 'IOS',
},
};
```
--------------------------------
### No-auth Tenant Configuration
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/configuration.md
Example configuration for a no-auth tenant, requiring only the tenantAppId and product IDs.
```typescript
const config: BiltIapConfig = {
tenantAppId: '11111111-1111-1111-1111-111111111111',
productIds: [{ id: 'com.example.pro.monthly' }],
};
```
--------------------------------
### Supabase-auth Tenant Configuration
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/configuration.md
Example configuration for a Supabase-auth tenant, including tenantAppId, a getAccessToken function, and product IDs.
```typescript
import { supabase } from './supabaseClient';
import type { BiltIapConfig } from '@biltme/iap';
const config: BiltIapConfig = {
tenantAppId: '11111111-1111-1111-1111-111111111111',
getAccessToken: async () => {
const { data } = await supabase.auth.getSession();
return data.session?.access_token ?? null;
},
productIds: [
{ id: 'com.example.pro.monthly' },
{ id: 'com.example.pro.annual' },
],
onError: (err) => console.warn('[BiltIAP]', err),
};
```
--------------------------------
### Install @biltme/iap
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Install the @biltme/iap package using npm or bun. Ensure peer dependencies like react, react-native, expo-iap, and @react-native-async-storage/async-storage are installed at the correct versions.
```sh
npm install @biltme/iap
# or
bun add @biltme/iap
```
--------------------------------
### Build Paywall UI with Hooks
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Implement a paywall screen using `useBiltIAP` and `useEntitlement` hooks. This example shows how to display products, handle purchases, restore purchases, and manage loading and entitlement states.
```typescript
import React from 'react';
import { View, Text, Button, ActivityIndicator } from 'react-native';
import { useBiltIAP, useEntitlement, BiltIapError } from '@biltme/iap';
export function PaywallScreen() {
const {
initialized,
loading,
products,
purchasing,
purchaseProduct,
restorePurchases,
} = useBiltIAP();
const pro = useEntitlement('pro');
// Show loading state until initialized
if (!initialized) {
return (
);
}
// If user has pro, show different UI
if (pro.active) {
return (
You have Pro!
);
}
// Show products and purchase buttons
return (
Upgrade to Pro
{products.map((product) => (
);
async function handlePurchase(productId: string) {
try {
await purchaseProduct(productId);
// Purchase succeeded, pro.active should now be true
} catch (err) {
if (err instanceof BiltIapError) {
handleError(err);
} else {
throw err;
}
}
}
async function handleRestore() {
try {
await restorePurchases();
} catch (err) {
if (err instanceof BiltIapError) {
handleError(err);
}
}
}
function handleError(err: BiltIapError) {
switch (err.code) {
case 'purchase_cancelled':
// User dismissed, not an error
break;
case 'network_error':
alert('Network error. Please check connectivity.');
break;
default:
alert(`Error: ${err.message}`);
}
}
}
```
--------------------------------
### BiltIapProvider Usage Example
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapProvider.md
Demonstrates how to wrap your application with BiltIapProvider and use the useBiltIAP hook to access products, purchase items, and restore purchases.
```typescript
import React, { useMemo } from 'react';
import { BiltIapProvider, useBiltIAP } from '@biltme/iap';
import type { BiltIapConfig } from '@biltme/iap';
export default function App() {
const config: BiltIapConfig = useMemo(() => ({
tenantAppId: '11111111-1111-1111-1111-111111111111',
getAccessToken: async () => {
const token = await auth.getToken();
return token ?? null;
},
productIds: [
{ id: 'com.example.pro.monthly' },
{ id: 'com.example.pro.annual' },
],
onError: (err) => {
console.warn('[BiltIAP Error]', {
code: err.code,
message: err.message,
retryable: err.retryable,
});
},
}), []);
return (
);
}
function PaywallScreen() {
const { initialized, products, purchaseProduct, restorePurchases } = useBiltIAP();
if (!initialized) {
return ;
}
return (
<>
{products.map((product) => (
purchaseProduct(product.id)}
/>
))}
restorePurchases()} />
>
);
}
```
--------------------------------
### GET /iap/v1/bootstrap
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
Initializes the billing session for the current user. This is typically called automatically on provider mount.
```APIDOC
## GET /iap/v1/bootstrap
### Description
Initialize the billing session for the current user.
### Method
GET
### Endpoint
/iap/v1/bootstrap
### Parameters
#### Request Body
No request body. Uses headers to identify the user.
### Response
#### Success Response (200)
- **appAccountToken** (string) - Stable per-user token the backend tags transactions with. Persists across sign-out and sign-in.
- **entitlements** (EntitlementMap) - The current entitlements for this user. See [types.md#entitlementmap](./types.md#entitlementmap).
#### Response Example
```json
{
"ok": true,
"data": {
"appAccountToken": "stable-per-user-token",
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS"
},
"premium": {
"active": false,
"status": "expired",
"platform": "IOS"
}
}
}
}
```
### Errors
- **unauthorized**: Invalid or missing authentication. (Retryable: No)
- **billing_user_not_found**: No user record exists on the backend for this principal. (Retryable: No)
- **internal_error**: Unexpected backend failure. (Retryable: Yes)
```
--------------------------------
### Integrate the SDK with BiltIapProvider
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/README.md
Set up the BiltIapProvider with your configuration to manage in-app purchases. Ensure your configuration includes tenantAppId, a function to get access tokens, and product IDs.
```typescript
import { BiltIapProvider, useBiltIAP } from '@biltme/iap';
const config = {
tenantAppId: '...',
getAccessToken: async () => auth.getToken(),
productIds: [{ id: 'com.example.pro.monthly' }],
};
export function App() {
return (
);
}
```
--------------------------------
### Initiate a Product Purchase
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useBiltIAP.md
Use this function to start the purchase flow for a specific product. It handles opening the native purchase sheet and ingesting the receipt. Ensure the product ID exists in the `products` array.
```typescript
const { purchasing, purchaseProduct, products } = useBiltIAP();
const product = products.find(p => p.id === 'com.example.pro');
if (!product) {
return Product unavailable;
}
return (
{
try {
await purchaseProduct(product.id);
// Purchase succeeded, entitlements updated
} catch (err) {
if (err instanceof BiltIapError) {
if (err.code === 'purchase_cancelled') {
// User dismissed sheet, not an error
return;
}
console.error('Purchase failed:', err.message);
}
}
}}
title={purchasing ? 'Purchasing...' : `Buy ${product.title}`}
/>
);
```
--------------------------------
### GET /iap/v1/bootstrap Success Response
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
This JSON structure represents a successful response from the GET /iap/v1/bootstrap endpoint, which initializes the billing session. It includes a stable user token and current entitlements.
```json
{
"ok": true,
"data": {
"appAccountToken": "stable-per-user-token",
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS"
},
"premium": {
"active": false,
"status": "expired",
"platform": "IOS"
}
}
}
}
```
--------------------------------
### Quick Start with BiltIapProvider and Hooks
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Set up the BiltIapProvider with your configuration, including tenant ID, access token retrieval, product IDs, and error handling. Use useBiltIAP and useEntitlement hooks to manage products, purchases, and user entitlements within your React application.
```tsx
import React from 'react';
import { BiltIapProvider, useBiltIAP, useEntitlement } from '@biltme/iap';
import type { BiltIapConfig } from '@biltme/iap';
const config: BiltIapConfig = {
tenantAppId: '11111111-1111-1111-1111-111111111111',
getAccessToken: async () => auth.getToken(),
productIds: [{ id: 'com.example.pro.monthly' }],
onError: (err) =>
console.warn('[BiltIAP]', {
code: err.code,
message: err.message,
retryable: err.retryable,
requestId: err.requestId,
cause: err.cause,
}),
};
export default function App() {
return (
);
}
function PaywallScreen() {
const { initialized, products, purchaseProduct, restorePurchases } = useBiltIAP();
const pro = useEntitlement('pro');
if (!initialized) return null;
if (pro.active) return ;
return (
<>
{products.map((p) => (
purchaseProduct(p.id)}
/>
))}
restorePurchases()} />
>
);
}
```
--------------------------------
### Restore Purchases Response
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
This is an example of a successful response after restoring purchases. It contains the updated entitlements for the user.
```json
{
"ok": true,
"data": {
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS"
}
}
}
}
```
--------------------------------
### Configure Mock Mode for Testing
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Set `billingEnvironment: 'mock'` in `BiltIapConfig` to enable mock mode for local testing without real transactions. Includes example product configuration.
```typescript
const billingConfig: BiltIapConfig = {
billingEnvironment: 'mock',
productIds: [
{
id: 'com.myapp.pro.monthly',
type: 'subs',
title: 'Pro Monthly',
displayPrice: '$9.99/month',
price: 9.99,
currency: 'USD',
},
],
// ...
};
```
--------------------------------
### No-Auth Tenant Billing Configuration
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Configure the BiltIapProvider for a tenant without a specific authentication provider. This setup is simpler and only requires the tenant ID and product IDs.
```typescript
import type { BiltIapConfig } from '@biltme/iap';
export const billingConfig: BiltIapConfig = {
tenantAppId: process.env.EXPO_PUBLIC_BILT_TENANT_ID,
productIds: [
{ id: 'com.myapp.pro.monthly' },
{ id: 'com.myapp.pro.annual' },
],
onError: (err) => {
console.warn('[IAP Error]', err);
},
};
```
--------------------------------
### Restore Purchases Client-Side Call
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
Example of how to call the `restorePurchases` function from the `useBiltIAP` hook in your client-side application.
```typescript
const { restorePurchases } = useBiltIAP();
await restorePurchases();
```
--------------------------------
### Handling BiltIapError in Purchase Operations
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/errors.md
Demonstrates how to catch and inspect BiltIapError instances after a purchase attempt. This example shows how to log specific error details like code, message, retryable status, and request ID.
```typescript
const { purchaseProduct } = useBiltIAP();
try {
await purchaseProduct('com.example.pro.monthly');
} catch (err) {
if (err instanceof BiltIapError) {
console.error('Purchase failed:', {
code: err.code,
message: err.message,
retryable: err.retryable,
requestId: err.requestId,
});
}
}
```
--------------------------------
### Bootstrap Network Call Headers and Response
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Details the request headers and expected response structure for the GET /iap/v1/bootstrap call, including tenant ID, billing environment, and authorization or anonymous user ID.
```plaintext
2.1 Request headers:
X-Bilt-Tenant-App-Id: config.tenantAppId
X-Bilt-Billing-Environment: config.billingEnvironment
Authorization: Bearer (if authenticated)
OR
X-Bilt-Anonymous-App-User-Id: (if anonymous)
2.2 Response:
├─ appAccountToken (stable per-user token)
└─ entitlements (EntitlementMap)
2.3 State update:
├─ Set appAccountToken
├─ Set entitlements
├─ Set loading: false
└─ Trigger re-render
```
--------------------------------
### Product Store Initialization - Mock Mode
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Handles product store initialization in mock mode by synthesizing product metadata from IDs or using explicit metadata, and setting the products array and store availability.
```plaintext
3.1 For each entry in config.productIds:
├─ If entry is object with explicit metadata → use it
├─ If entry is object with only id → synthesize from SKU heuristics
│ ├─ "lifetime" or "one-time" in id → type: 'in-app'
│ ├─ "inapp" in id → type: 'in-app'
│ └─ Otherwise → type: 'subs'
└─ Synthesize StoreProduct with display price
3.2 Set products array
3.3 Set storeAvailable: true
```
--------------------------------
### Product Store Initialization - Production Mode (iOS)
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Initializes the product store on iOS by calling ExpoIap.initConnection() and fetching products using ExpoIap.fetchProducts(). Handles success and failure scenarios.
```plaintext
3.1 Call ExpoIap.initConnection()
├─ On success → storeAvailable: true
└─ On failure → storeAvailable: false, storeUnavailableReason: error message
3.2 Call ExpoIap.fetchProducts({ skus, type: 'all' })
├─ On success → Set products array, infer type from typeIOS
└─ On failure → products: [], storeUnavailable: true
3.3 Set initialized: true
```
--------------------------------
### GET /iap/v1/entitlements Success Response
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
This JSON structure represents a successful response from the GET /iap/v1/entitlements endpoint, which fetches the current user entitlements. It includes an updated entitlements map with expiration dates.
```json
{
"ok": true,
"data": {
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS",
"expirationDate": 1693526400000
}
}
}
}
```
--------------------------------
### Local or Staging Backend Configuration
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/configuration.md
Configure the SDK to point to a local or staging backend URL. Ensure the tenant application ID and access token retrieval function are correctly set.
```typescript
const config: BiltIapConfig = {
backendUrl: 'http://127.0.0.1:8099',
tenantAppId: '11111111-1111-1111-1111-111111111111',
getAccessToken: async () => auth.getToken(),
productIds: [{ id: 'com.example.pro.monthly' }],
};
```
--------------------------------
### Handle Purchases with useBiltIAP
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/README.md
Initiate a product purchase using the purchaseProduct method from the useBiltIAP hook. This function handles the purchase flow.
```typescript
const { purchaseProduct, purchasing } = useBiltIAP();
await purchaseProduct('com.example.pro.monthly');
```
--------------------------------
### Ingest Purchase Response
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
This is an example of a successful response after ingesting a purchase. It indicates whether to finish the transaction and the updated entitlements.
```json
{
"ok": true,
"data": {
"finishTransaction": true,
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS"
}
}
}
}
```
--------------------------------
### Manual Entitlement Refresh
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
Example of manually invoking the refreshEntitlements function from the useBiltIAP hook to fetch the latest user entitlements.
```typescript
const { refreshEntitlements } = useBiltIAP();
await refreshEntitlements();
```
--------------------------------
### GET /iap/v1/entitlements
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/endpoints.md
Fetches the current entitlements for the billing principal. This is called when the app returns to the foreground or manually via refreshEntitlements().
```APIDOC
## GET /iap/v1/entitlements
### Description
Fetch the current entitlements for the billing principal.
### Method
GET
### Endpoint
/iap/v1/entitlements
### Parameters
#### Request Body
No request body. Uses headers to identify the user.
### Response
#### Success Response (200)
- **entitlements** (EntitlementMap) - Updated entitlements map. See [types.md#entitlementmap](./types.md#entitlementmap).
#### Response Example
```json
{
"ok": true,
"data": {
"entitlements": {
"pro": {
"active": true,
"status": "active",
"platform": "IOS",
"expirationDate": 1693526400000
}
}
}
}
```
### Errors
- **unauthorized**: Invalid or missing authentication. (Retryable: No)
- **internal_error**: Unexpected backend failure. (Retryable: Yes)
```
--------------------------------
### Purchase Flow Diagram
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Illustrates the sequence of events and checks during a user-initiated purchase. Includes handling for initialization, store availability, product validation, anonymous user linking, and production vs. mock mode execution.
```text
User Action: purchaseProduct('com.example.pro.monthly')
│
├─ [Check] initialized? If not → throw 'not_initialized'
├─ [Check] storeAvailable? If not → throw 'store_not_available'
├─ [Check] product in products? If not → throw 'product_not_found'
│
├─ [Auto-link] If principalType === 'anonymous' && getAccessToken() now returns token
│ └─ Call POST /iap/v1/link first
│ ├─ On success → Update principalType to 'authenticated'
│ └─ On failure → Throw error (user not signed in properly)
│
├─ Set purchasing: true
│
├─ [Production mode] Call ExpoIap.requestPurchase()
│ │
│ └─ User sees native purchase sheet
│ ├─ [User taps Buy]
│ │ └─ Call purchaseUpdatedListener (see below)
│ │
│ ├─ [User taps Cancel]
│ │ └─ Call purchaseErrorListener with 'user-cancelled'
│ │ └─ Catch and drop (not an error)
│ │
│ └─ [Ask-to-buy enabled]
│ └─ Call purchaseErrorListener with 'pending'
│ └─ Throw 'purchase_pending'
│
├─ [Mock mode] Generate synthetic receipt
│ └─ Call POST /iap/v1/purchases/ingest directly
│
└─ Set purchasing: false
```
--------------------------------
### Type-Safe Error Handling with Switch Statement
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapError.md
Provides a comprehensive example of handling different BiltIapError codes using a switch statement for specific user feedback.
```typescript
import { BiltIapError } from '@biltme/iap';
const { purchaseProduct } = useBiltIAP();
try {
await purchaseProduct('com.example.pro.monthly');
} catch (err) {
if (!(err instanceof BiltIapError)) {
throw err; // Re-throw non-SDK errors
}
// Now safe to access err.code, err.retryable, etc.
switch (err.code) {
case 'purchase_cancelled':
// Expected flow, no error needed
break;
case 'not_initialized':
showError('App not ready. Please wait and try again.');
break;
case 'network_error':
showError('Network error. Please check connectivity.');
break;
default:
showError(`Purchase failed: ${err.message}`);
}
}
```
--------------------------------
### Wrap Application with BiltIapProvider
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Use the BiltIapProvider to wrap your application or relevantส่วน. It initializes the billing system on mount and cleans up listeners on unmount.
```tsx
{children}
```
--------------------------------
### Mock Mode with Explicit Product Metadata
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/configuration.md
Use mock mode for testing with detailed product metadata. This includes product ID, type, title, description, display price, actual price, and currency.
```typescript
const config: BiltIapConfig = {
tenantAppId: '11111111-1111-1111-1111-111111111111',
billingEnvironment: 'mock',
productIds: [
{
id: 'com.buildingpp.subtrackr.aaa',
type: 'subs',
title: 'AAA Subscription',
description: 'Annual subscription to AAA features',
displayPrice: '$49.99/year',
price: 49.99,
currency: 'USD',
},
{
id: 'com.buildingpp.subtrackr.pro_monthly',
},
],
};
```
--------------------------------
### Combining useEntitlement with useBiltIAP for Purchases
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useEntitlement.md
Illustrates how to use `useEntitlement` to check for an active entitlement and `useBiltIAP` to initiate a product purchase if the entitlement is not active. It fetches product details and handles the purchasing state.
```typescript
import { useEntitlement, useBiltIAP } from '@biltme/iap';
function Paywall() {
const pro = useEntitlement('pro');
const { purchaseProduct, purchasing, products } = useBiltIAP();
if (pro.loading) return ;
if (pro.active) {
return ;
}
const product = products.find(p => p.id === 'com.example.pro');
return (
purchaseProduct('com.example.pro')}
title={`Buy ${product?.title ?? 'Pro'}`}
/>
);
}
```
--------------------------------
### useBiltIAP()
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/README.md
Main hook for accessing the billing state and API methods. Provides access to purchase products, check purchasing status, and manage entitlements.
```APIDOC
## useBiltIAP()
### Description
Main hook for accessing the billing state and API methods. Provides access to purchase products, check purchasing status, and manage entitlements.
### Usage
```typescript
import { useBiltIAP } from '@biltme/iap';
const { purchaseProduct, purchasing, error } = useBiltIAP();
async function handlePurchase() {
try {
await purchaseProduct('com.example.pro.monthly');
} catch (err) {
console.error('Purchase failed:', err);
}
}
```
### Methods
- `purchaseProduct(productId: string): Promise`: Initiates a product purchase.
- `restorePurchases(): Promise`: Restores previous purchases.
### State
- `purchasing: boolean`: Indicates if a purchase is currently in progress.
- `error: BiltIapError | null`: Contains error information if a purchase fails.
```
--------------------------------
### Product Store Initialization - Android/Web
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Sets product store availability to false for Android and Web platforms, indicating that native store operations are not supported.
```plaintext
3.1 storeAvailable: false
3.2 storeUnavailableReason: "Android not supported" / "web platform"
3.3 products: [] (can still fetch from server in config if desired)
```
--------------------------------
### purchaseProduct
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useBiltIAP.md
Initiates the purchase of a specified product. This function handles both production and mock modes, opening the native purchase sheet or sending synthetic proof respectively. It also automatically links anonymous purchases if the user becomes authenticated during the process.
```APIDOC
## purchaseProduct(productId: string): Promise
### Description
Initiate a purchase of the specified product. This function handles the native purchase flow and receipt ingestion.
### Method
`purchaseProduct` (SDK Method)
### Parameters
#### Path Parameters
None
#### Query Parameters
None
#### Request Body
None
### Parameters
- **productId** (`string`) - Required - The product ID (e.g., `"com.example.pro.monthly"`). Must exist in the `products` array.
### Returns
`Promise` - Resolves when the purchase ingest completes successfully. Rejects on error.
### Errors thrown
- `not_initialized`: Provider not finished init or store unavailable.
- `product_not_found`: Product ID not in `products` array.
- `purchase_cancelled`: User dismissed the purchase sheet.
- `purchase_pending`: Purchase is pending approval (Ask-to-Buy).
- `purchase_failed`: Native purchase flow failed.
- `network_error`: Ingest call failed (may be auto-retried).
- `product_not_configured`: Product not in tenant catalog (from backend).
- `ownership_mismatch`: Receipt belongs to different account (from backend).
### Usage Example
```typescript
const { purchasing, purchaseProduct, products } = useBiltIAP();
const product = products.find(p => p.id === 'com.example.pro');
if (!product) {
return Product unavailable;
}
return (
{
try {
await purchaseProduct(product.id);
// Purchase succeeded, entitlements updated
} catch (err) {
if (err instanceof BiltIapError) {
if (err.code === 'purchase_cancelled') {
// User dismissed sheet, not an error
return;
}
console.error('Purchase failed:', err.message);
}
}
}}
title={purchasing ? 'Purchasing...' : `Buy ${product.title}`}
/>
);
```
```
--------------------------------
### Memoizing BiltIapProvider Config
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapProvider.md
Ensures the configuration object for BiltIapProvider is memoized using `useMemo` to prevent unnecessary re-reads on every render. This is the correct way to pass configuration.
```typescript
// ✅ Correct: config is memoized
const config = useMemo(() => ({ ... }), [dependencies]);
return {children};
// ❌ Wrong: new config object on every render
return {children};
```
--------------------------------
### Restore Flow Diagram
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Details the steps involved in restoring purchases, initiated by the `restorePurchases()` action. It covers checking initialization, calling native restore functions, ingesting receipts, backend reconciliation, and updating entitlements.
```text
User action: restorePurchases()
1. [Check] initialized? If not → throw 'not_initialized'
2. Set restoring: true
3. Call ExpoIap.restorePurchases()
└─ Refresh receipts in native store
4. Call ExpoIap.getAvailablePurchases({ onlyIncludeActiveItemsIOS: true })
└─ Get all active transactions
5. For each unique transaction
└─ Call POST /iap/v1/purchases/ingest
├─ Retryable errors → add to queue
└─ Non-retryable → drop
6. Call POST /iap/v1/restore
└─ Let backend reconcile (mark expired, etc.)
7. Update entitlements from response
8. Set restoring: false
```
--------------------------------
### Setting BiltIapProvider Billing Environment
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapProvider.md
Shows how to explicitly set the billing environment for BiltIapProvider to either 'mock' or 'production' using the `billingEnvironment` option in the configuration.
```typescript
const config: BiltIapConfig = {
billingEnvironment: 'mock', // or 'production'
// ...
};
```
--------------------------------
### Supabase-Auth Tenant Billing Configuration
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Configure the BiltIapProvider for a tenant using Supabase for authentication. This includes fetching access tokens and defining product IDs.
```typescript
import { supabase } from './lib/supabaseClient';
import type { BiltIapConfig } from '@biltme/iap';
export const billingConfig: BiltIapConfig = {
tenantAppId: process.env.EXPO_PUBLIC_BILT_TENANT_ID,
getAccessToken: async () => {
const { data } = await supabase.auth.getSession();
return data.session?.access_token ?? null;
},
productIds: [
{ id: 'com.myapp.pro.monthly' },
{ id: 'com.myapp.pro.annual' },
],
billingEnvironment: __DEV__ ? 'mock' : 'production',
onError: (err) => {
console.warn('[IAP Error]', {
code: err.code,
message: err.message,
requestId: err.requestId,
});
},
};
```
--------------------------------
### Configure BiltIap with Mock Product Metadata
Source: https://github.com/buildingapplications/iap/blob/main/README.md
This configuration is for mock mode, allowing explicit definition of product metadata like type, title, and description. It's useful for testing UI elements before production.
```tsx
const config: BiltIapConfig = {
tenantAppId: '11111111-1111-1111-1111-111111111111',
billingEnvironment: 'mock',
productIds: [
{
id: 'com.buildingpp.subtrackr.aaa',
type: 'subs',
title: 'AAA',
description: 'Mock subscription product',
},
{ id: 'com.buildingpp.subtrackr.pro_monthly_2' },
],
};
```
--------------------------------
### Wrap App with BiltIapProvider
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Integrate the BiltIapProvider into your root component to enable in-app purchase functionality throughout the app. Memoize the config object for performance.
```typescript
import React, { useMemo } from 'react';
import { BiltIapProvider } from '@biltme/iap';
import { billingConfig } from './config/billing';
export function RootLayout() {
const memoizedConfig = useMemo(() => billingConfig, []);
return (
);
}
```
--------------------------------
### Handle Product Not Found Error
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/errors.md
Verify that a product ID exists in the fetched products list before attempting a purchase. This prevents errors caused by typos or missing product configurations.
```typescript
const { products } = useBiltIAP();
const productId = 'com.example.pro.monthly';
if (!products.some(p => p.id === productId)) {
return Product not available;
}
return purchaseProduct(productId)} />;
```
--------------------------------
### ConfiguredProduct Type Definition
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/configuration.md
Defines the structure for individual product configurations. In mock mode, additional fields can be provided for custom metadata.
```typescript
type ConfiguredProduct = {
id: string;
type?: 'subs' | 'in-app';
title?: string;
description?: string;
displayPrice?: string;
price?: number;
currency?: string;
};
```
--------------------------------
### Equivalence of useEntitlement and useBiltIAP for Single Entitlement Check
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useEntitlement.md
Shows the direct equivalence between using `useEntitlement('code')` and manually accessing the `entitlements` and `initialized` state from `useBiltIAP()`. This highlights that `useEntitlement` is a convenient abstraction.
```typescript
// These are equivalent:
const pro1 = useEntitlement('pro');
const { entitlements, initialized } = useBiltIAP();
const pro2 = {
active: entitlements['pro']?.active ?? false,
status: entitlements['pro']?.status,
entitlement: entitlements['pro'],
loading: !initialized,
};
// pro1 === pro2
```
--------------------------------
### Dynamic Access Token Callback for BiltIapProvider
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapProvider.md
Illustrates how the `getAccessToken` callback within the BiltIapProvider configuration can be re-invoked on every request, enabling token rotation without re-creating the provider.
```typescript
const config: BiltIapConfig = {
getAccessToken: async () => {
// Called on every request
const session = await supabase.auth.getSession();
return session?.access_token ?? null;
},
// ...
};
```
--------------------------------
### Check Initialization Before Purchase
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/errors.md
Ensure the IAP provider is initialized before calling purchase methods. This prevents errors related to uninitialized states.
```typescript
const { initialized, purchaseProduct } = useBiltIAP();
if (!initialized) {
return ;
}
return purchaseProduct('com.example.pro')} />;
```
--------------------------------
### Foreground Refresh Logic
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Describes the process triggered when the application becomes active after being initialized. It involves fetching the latest entitlements from the backend and flushing the pending retry queue.
```text
1. Call GET /iap/v1/entitlements
├─ On success → Update entitlements, trigger re-render
└─ On failure → Call onError(), keep existing entitlements
2. Flush retry queue
└─ Retry any pending ingest payloads
```
--------------------------------
### useBiltIAP()
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Provides access to Bilt IAP functionalities, including methods to manage the retry queue.
```APIDOC
## `useBiltIAP()`
### Description
Provides access to Bilt IAP functionalities, including methods to manage the retry queue.
### Methods
#### `flushRetryQueue(): Promise`
##### Description
Force-flush the retry queue now (e.g. when the app regains connectivity).
##### Method Signature
`flushRetryQueue(): Promise`
##### Returns
A promise that resolves when the retry queue has been flushed.
```
--------------------------------
### BiltIapProvider Component
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/BiltIapProvider.md
The BiltIapProvider component initializes the in-app purchase system. It requires a configuration object and accepts child components that can then utilize the `useBiltIAP` and `useEntitlement` hooks.
```APIDOC
## BiltIapProvider
### Description
React Context provider that initializes the in-app purchase system and makes billing state and API methods available to child components via hooks.
### Signature
```typescript
function BiltIapProvider(props: {
config: BiltIapConfig;
children: React.ReactNode;
}): JSX.Element;
```
### Parameters
#### Parameters
- **config** (`BiltIapConfig`) - Required - Configuration object. See [configuration.md](../configuration.md) for all options.
- **children** (`React.ReactNode`) - Required - Child components that will be able to use `useBiltIAP()` and `useEntitlement()` hooks.
```
--------------------------------
### Using useBiltIAP within BiltIapProvider
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useBiltIAP.md
Demonstrates the correct placement of the useBiltIAP hook inside a component rendered within BiltIapProvider. Calling it outside the provider will result in an error.
```typescript
import { BiltIapProvider, useBiltIAP } from '@biltme/iap';
export function App() {
return (
);
}
function PaywallScreen() {
// ✅ This works
const { initialized, products, purchaseProduct } = useBiltIAP();
// ...
}
// ❌ This throws: "useBiltIAP must be used inside "
function OutsideProvider() {
const state = useBiltIAP();
}
```
--------------------------------
### useBiltIAP() Hook
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useBiltIAP.md
Main hook to access the billing state and API methods for Bilt In-App Purchases. Must be called inside a BiltIapProvider.
```APIDOC
## useBiltIAP()
### Description
Main hook to access the billing state and API methods for Bilt In-App Purchases. Must be called inside a BiltIapProvider.
### Signature
```typescript
function useBiltIAP(): BiltIapState & BiltIapApi;
```
### Return Type
Returns an object containing both immutable state and mutable API methods.
#### State Properties
- **initialized** (boolean) - `true` once bootstrap and store init complete (success or failure). Safe to show UI once `true`.
- **loading** (boolean) - `true` until initial bootstrap resolves. Use for full-screen loading spinners.
- **principalType** ('authenticated' | 'anonymous') - Current billing principal: derived from `getAccessToken()` or anonymous ID.
- **currentAppUserId** (string | undefined) - Stable backend user ID for the active principal. Set after bootstrap.
- **billingEnvironment** ('mock' | 'production') - Runtime environment. Sent as `X-Bilt-Billing-Environment` header.
- **purchasing** (boolean) - `true` while `purchaseProduct()` is executing. Use to disable purchase buttons.
- **restoring** (boolean) - `true` while `restorePurchases()` is executing. Use to disable restore button.
- **entitlements** (EntitlementMap) - Backend-authoritative map keyed by entitlement code (e.g., "pro"). Empty until bootstrap.
- **appAccountToken** (string | undefined) - Stable per-user token the backend uses to tag transactions. Set after bootstrap.
- **products** (StoreProduct[]) - Products from the store or mock mode. Empty on Android/web or if fetch failed.
- **storeAvailable** (boolean) - `true` if native StoreKit is available and initialized. `false` on web, Expo Go, or if init failed.
- **storeUnavailableReason** (string | undefined) - Human-readable reason store is unavailable (e.g., "web platform", "no native module").
- **lastError** (BiltIapError | undefined) - Last error surfaced by the provider. Updated by `onError` callbacks.
- **pendingRetries** (number) - Count of ingest payloads in the offline retry queue. Useful for retry banners.
#### API Methods
- **purchaseProduct**(productId: string): Promise - Purchases a product with the given ID.
- **restorePurchases**(): Promise - Restores previously made purchases.
- **linkAnonymousPurchasesToCurrentUser**(): Promise - Links anonymous purchases to the current authenticated user.
- **refreshEntitlements**(): Promise - Refreshes the user's entitlements from the backend.
- **hasEntitlement**(code: string): boolean - Checks if the user has a specific entitlement.
- **openManageSubscriptions**(): Promise - Opens the native manage subscriptions page.
- **flushRetryQueue**(): Promise - Flushes the offline retry queue.
```
--------------------------------
### Bootstrap Endpoint
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Retrieves bootstrap information for the IAP service.
```APIDOC
## GET /iap/v1/bootstrap
### Description
Retrieves bootstrap information. This endpoint is used during the mounting process.
### Method
GET
### Endpoint
/iap/v1/bootstrap
### Request Headers
- `Content-Type: application/json`
- `X-Bilt-Tenant-App-Id: `
- `X-Bilt-Billing-Environment: mock | production`
- `Authorization: Bearer ` (when authenticated)
- `X-Bilt-Anonymous-App-User-Id: ` (when anonymous)
### Response
#### Success Response (200)
- `ok` (boolean) - Indicates success.
- `data` (T) - The bootstrap data.
#### Failure Response
- `ok` (boolean) - Indicates failure.
- `error` (object) - Contains error details (`code`, `message`, `retryable?`, `requestId?`).
```
--------------------------------
### Open Manage Subscriptions with useBiltIAP
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/api-reference/useBiltIAP.md
Call openManageSubscriptions to open the native subscription management interface. This function is platform-dependent, with specific behavior for iOS and a no-op for Android/web.
```typescript
const { openManageSubscriptions } = useBiltIAP();
return (
openManageSubscriptions()}
title="Manage Subscriptions"
/>
);
```
--------------------------------
### Native Event Listener Subscription
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Subscribes to native purchase update and error listeners from ExpoIap, and sets up an AppState listener for foreground refresh events.
```plaintext
5.1 ExpoIap.purchaseUpdatedListener
└─ Called when a purchase completes
└─ See Purchase-Updated Listener below
5.2 ExpoIap.purchaseErrorListener
└─ Called when a purchase fails
└─ See Purchase-Error Listener below
5.3 AppState.addEventListener('change')
└─ Called when app foreground/background changes
└─ See Foreground Refresh below
```
--------------------------------
### Configure Global Error Logging
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/INTEGRATION_GUIDE.md
Set up a global `onError` callback in the `BiltIapConfig` to log errors to the console and send them to a monitoring service like Sentry.
```typescript
import * as Sentry from '@sentry/react-native';
const billingConfig: BiltIapConfig = {
// ...
onError: (err) => {
// Log to console
console.warn('[IAP Error]', {
code: err.code,
message: err.message,
retryable: err.retryable,
requestId: err.requestId,
});
// Send to monitoring
Sentry.captureException(err, {
level: err.retryable ? 'warning' : 'error',
tags: {
code: err.code,
retryable: err.retryable.toString(),
},
contexts: {
iap: {
requestId: err.requestId,
},
},
});
},
};
```
--------------------------------
### No-Auth Tenant Configuration
Source: https://github.com/buildingapplications/iap/blob/main/README.md
Configure Bilt IAP for a tenant with no authentication. Omit `getAccessToken` entirely. The SDK uses anonymous billing identities.
```tsx
const config: BiltIapConfig = {
tenantAppId: '...',
productIds: [{ id: 'com.example.pro.monthly' }],
};
```
--------------------------------
### Purchase-Updated Listener Logic
Source: https://github.com/buildingapplications/iap/blob/main/_autodocs/LIFECYCLE.md
Details the steps executed when a purchase is successfully updated via the `purchaseUpdatedListener`. Covers receipt extraction, backend ingestion, entitlement updates, transaction finishing, and retry logic for failed ingestions.
```text
1. Extract receipt and transactionId
2. Call POST /iap/v1/purchases/ingest
├─ On success
│ ├─ Response includes finishTransaction: true/false
│ ├─ Update entitlements
│ ├─ If finishTransaction: true → call ExpoIap.finishTransaction()
│ └─ Trigger UI re-render (entitlements changed)
│
└─ On failure
├─ If retryable
│ ├─ Add to retry queue
│ ├─ Persist to AsyncStorage
│ ├─ Increment pendingRetries
│ └─ Schedule flush (exponential backoff)
│
└─ If non-retryable
├─ Drop payload
├─ Call onError(error)
└─ Set lastError
```