- Follow the Angular Conventional Commits format:
<type>(<scope>): <subject> - Allowed types:
feat,fix,docs,style,refactor,perf,test,chore - Scope is optional but recommended (for example:
auth,api,ui) - Subject must be imperative, lowercase, without a trailing period, and roughly 50 characters
- Wrap commit body lines near 72 characters and include footers such as
BREAKING CHANGE:orCloses #123when needed
- Use Bun exclusively. Run installs with
bun install, scripts withbun run <script>, add deps viabun add/bun add -d. - Do not suggest or create
package-lock.jsonoryarn.lock;bun.lockis the single source of truth.
CRITICAL WARNING: Never modify the iOS platform version in ios/ExpoIap.podspec
- iOS platform version MUST remain at
13.4even though the code requires iOS 15.0+ - Changing iOS to
15.0can cause expo prebuild to exclude the module in certain Expo SDKs (known bug) - See issue: #168
- This is kept at
13.4for compatibility with affected Expo SDKs - The actual iOS 15.0+ requirement is enforced at build time via @available annotations
- Users must ensure their app target is set to iOS 15.0 or higher:
- app.json:
"expo": { "ios": { "deploymentTarget": "15.0" } } - or app.config.ts:
ios: { deploymentTarget: '15.0' }
- app.json:
tvOS Exception: tvOS platform version MUST be 16.0 (not 13.4) because:
- The
openiapCocoaPods dependency requires tvOS 16.0 minimum - Setting tvOS to
13.4causes build failure due to dependency mismatch - This is a hard requirement from the dependency, not subject to the iOS workaround
Before committing any changes:
- Run
bun run lintto ensure code quality - Run
bun run typecheckto verify TypeScript types - Run
bun run testto verify all tests pass (Note: Usebun run test, notbun test) - IMPORTANT: Run tests in the example directory as well:
cd example && bun run test- Ensure all tests pass with 100% success rate
- Fix any failing tests before committing
- Only commit if all checks succeed
- Functions that only operate on one platform must carry the suffix:
nameIOSornameAndroid(e.g.getStorefrontIOS,deepLinkToSubscriptionsAndroid). - Cross-platform helpers should expose a single name and branch internally via
Platform.selector equivalent.
- iOS-related fields: Use
IOSsuffix (e.g.,displayNameIOS,discountsIOS,introductoryPriceIOS)- Exception: When an acronym appears at the end of a field name, use uppercase (e.g.,
quantityIOS,appBundleIdIOS, notquantityIos) - Platform-specific fields:
currencyCodeIOS,currencySymbolIOS,countryCodeIOS - Product fields:
isFamilyShareableIOS,jsonRepresentationIOS,subscriptionInfoIOS
- Exception: When an acronym appears at the end of a field name, use uppercase (e.g.,
- Android-related fields: Use
Androidsuffix (e.g.,nameAndroid)- Platform-specific fields:
oneTimePurchaseOfferDetailsAndroid,subscriptionOfferDetailsAndroid - In Android-specific types (e.g.,
ProductSubscriptionAndroidOfferDetails), keeppricingPhaseswithout suffix for consistency with Google Play Billing - In cross-platform types (e.g.,
SubscriptionOffer,DiscountOffer), usepricingPhasesAndroidsuffix to distinguish from common fields
- Platform-specific fields:
- Common fields: Fields shared across platforms go in Common types (e.g.,
ids,platform,debugDescription)- Use these for data that exists on both platforms without platform-specific variations
- iOS types: Use
IOSsuffix (e.g.,PurchaseIOS,ProductIOS) - Android types: Use descriptive prefixes to identify subtypes:
- ✅ Good:
ProductAndroidOneTimePurchaseOfferDetail,ProductSubscriptionAndroidOfferDetails,PurchaseAndroidState - ❌ Avoid:
OneTimePurchaseOfferDetails,SubscriptionOfferAndroid,PurchaseStateAndroid
- ✅ Good:
- General IAP types: Use
Iapprefix (e.g.,IapPurchase, notIAPPurchase)
- ID fields: Use
Idinstead ofID(e.g.,productId,transactionId, notproductID,transactionID) - Consistent naming: This applies to functions, types, and file names
- Deprecation: Fields without platform suffixes will be removed in v2.9.0
For complete type definitions and documentation, see: https://www.openiap.dev/docs/types
The library follows the OpenIAP type specifications with platform-specific extensions using iOS/Android suffixes.
Important:
src/types.tsis generated from the OpenIAP schema. Never edit this file manually or commit hand-written changes. After updating any*.graphqlschema, runbun run generate:types(or the equivalent script in your package manager) to refresh the file.
- Whenever you need Request/Params/Result types in the JS API surface (
src/index.ts, hooks, modules, examples), import them directly from the generatedsrc/types.ts(e.g.,MutationRequestPurchaseArgs,QueryFetchProductsArgs). Bind exported functions with the generatedQueryField/MutationFieldhelpers so their signatures stay in lockstep withtypes.tsinstead of redefining ad-hoc unions likeProductTypeInput.
- Conditional Rendering: Use ternary operator with null instead of logical AND
- ✅ Good:
{condition ? <Component /> : null} - ❌ Avoid:
{condition && <Component />}
- ✅ Good:
- Inside the
useIAPhook, most methods returnPromise<void>and update internal state. Do not design examples or implementations that expect data from these methods.- Examples:
fetchProducts,requestPurchase,getAvailablePurchases,getActiveSubscriptions. - After calling, consume state from the hook:
products,subscriptions,availablePurchases,activeSubscriptions, etc.
- Examples:
- Defined exceptions that DO return values in the hook:
hasActiveSubscriptions(subscriptionIds?) => Promise<boolean>
- The root (index) API is value-returning and can be awaited to receive data directly. Use root API when not using React state.
- Functions that depend on event results should use
requestprefix (e.g.,requestPurchase) - Follow OpenIAP terminology: https://www.openiap.dev/docs/apis#terminology
- Do not use generic prefixes like
get,find- refer to the official terminology
All implementations must follow the OpenIAP specification:
- APIs: https://www.openiap.dev/docs/apis
- Types: https://www.openiap.dev/docs/types
- Events: https://www.openiap.dev/docs/events
- Errors: https://www.openiap.dev/docs/errors
For new feature proposals:
- Before implementing, discuss at: https://github.com/hyochan/openiap.dev/discussions
- Get community feedback and consensus
- Ensure alignment with OpenIAP standards
- Implement following the agreed specification
The IapStatus struct provides standardized state management for OpenIAP operations:
public struct IapStatus {
public var loadings: LoadingStates = LoadingStates()
public var lastPurchaseResult: PurchaseResultData?
public var lastError: ErrorData?
// ...
}
public struct LoadingStates {
public var initConnection: Bool = false
public var fetchProducts: Bool = false
public var restorePurchases: Bool = false
public var purchasing: Set<String> = [] // Product IDs currently being purchased
}- OpenIapStore manages data state only, not UI state
- UI components should manage their own display state (alerts, sheets, etc.)
- Loading states are automatically managed within OpenIapStore
- Use
status.loadings.purchasing.contains(productId)to check if a specific product is being purchased - Use
status.isLoadingcomputed property to check if any operation is in progress
All error codes in expo-iap follow the OpenIAP specification and use kebab-case format:
export enum ErrorCode {
UserCancelled = 'user-cancelled', // NOT 'E_USER_CANCELLED'
NetworkError = 'network-error',
ItemUnavailable = 'item-unavailable',
AlreadyOwned = 'already-owned',
// ...
}Important:
- ✅ Use
ErrorCode.UserCancelledenum in TypeScript code - ✅ Error codes are kebab-case strings:
'user-cancelled' - ❌ Never use deprecated
E_prefix:'E_USER_CANCELLED' - ❌ Never use UPPERCASE format:
'USER_CANCELLED'
All errors returned from native modules have the following structure:
interface PurchaseError {
code: ErrorCode; // Standardized error code enum
message: string; // Human-readable error message
responseCode?: number; // Platform-specific response code
debugMessage?: string; // Additional debug information
productId?: string; // Product ID if applicable
}Example error object:
{
"code": "user-cancelled",
"message": "User cancelled the purchase",
"responseCode": 1,
"debugMessage": "User pressed cancel button",
"productId": "com.example.premium"
}Always use the ErrorCode enum for type-safe error handling:
import {useIAP, ErrorCode} from 'expo-iap';
const {requestPurchase} = useIAP({
onPurchaseError: (error) => {
// ✅ Correct - use ErrorCode enum
if (error.code === ErrorCode.UserCancelled) {
console.log('User cancelled');
return;
}
// ❌ Wrong - don't use string literals
if (error.code === 'E_USER_CANCELLED') {
/* ... */
}
if (error.code === 'USER_CANCELLED') {
/* ... */
}
// ✅ Correct - use switch with enum
switch (error.code) {
case ErrorCode.NetworkError:
showRetryDialog();
break;
case ErrorCode.ItemUnavailable:
showUnavailableMessage();
break;
default:
console.error(error.message);
}
},
});For complete error handling documentation, see:
- Title format: Use version number only (e.g.,
v2.8.7 - Feature Description, notExpo IAP v2.8.7 - Feature Description) - Heading format: Use version number only in headings (e.g.,
# v2.8.7 Release Notes, not# Expo IAP v2.8.7 Release Notes)