Skip to content

Commit 94be1d8

Browse files
authored
feat: update to OpenIAP v1.3.11 (#298)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Breaking Changes** * PurchaseState simplified: removed 'failed', 'restored', and 'deferred' in v3.4.0. * Old alternative-billing APIs deprecated; migrate to Billing Programs. * **New Features** * Android Billing Programs expanded (including user-choice-billing and external-payments) and new enableBillingProgramAndroid option; updated Android flows and UI examples. * **Documentation** * Large docs update: migration guide, API reference, guides, examples, blog release notes, and versioning. * **Chores** * Pre-commit adjusted to stop auto-re-adding files after linting. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 76bd2e7 commit 94be1d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+10861
-285
lines changed

.husky/pre-commit

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ else
1010
npm run lint:ci || { echo "Lint failed. Aborting commit." >&2; exit 1; }
1111
fi
1212

13-
# Re-add files that were modified by linting
14-
git add -u
15-
1613
# Run tests with coverage before committing to reduce CI failures
1714
echo "Running test coverage (pre-commit)…"
1815
if command -v bun >/dev/null 2>&1; then

android/src/main/java/expo/modules/iap/ExpoIapModule.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ class ExpoIapModule : Module() {
5858
get() = appContext.activityProvider?.currentActivity ?: throw Exceptions.MissingActivity()
5959

6060
private val openIap: OpenIapModule by lazy { OpenIapModule(context) }
61-
private val openIapStore: OpenIapStore by lazy { OpenIapStore(context) }
61+
62+
// Pass openIap directly to OpenIapStore to avoid reflection-based module loading
63+
private val openIapStore: OpenIapStore by lazy { OpenIapStore(openIap) }
6264
private var listenersAttached = false
6365
private val pendingEvents = ConcurrentLinkedQueue<Pair<String, Map<String, Any?>>>()
6466
private val connectionReady = AtomicBoolean(false)
@@ -388,6 +390,7 @@ class ExpoIapModule : Module() {
388390
}
389391
}
390392

393+
@Suppress("DEPRECATION")
391394
AsyncFunction("checkAlternativeBillingAvailabilityAndroid") { promise: Promise ->
392395
ExpoIapLog.payload("checkAlternativeBillingAvailabilityAndroid", null)
393396
scope.launch {
@@ -402,6 +405,7 @@ class ExpoIapModule : Module() {
402405
}
403406
}
404407

408+
@Suppress("DEPRECATION")
405409
AsyncFunction("showAlternativeBillingDialogAndroid") { promise: Promise ->
406410
ExpoIapLog.payload("showAlternativeBillingDialogAndroid", null)
407411
scope.launch {
@@ -425,6 +429,7 @@ class ExpoIapModule : Module() {
425429
}
426430
}
427431

432+
@Suppress("DEPRECATION")
428433
AsyncFunction("createAlternativeBillingTokenAndroid") { sku: String?, promise: Promise ->
429434
ExpoIapLog.payload("createAlternativeBillingTokenAndroid", mapOf("sku" to sku))
430435
scope.launch {
@@ -441,6 +446,7 @@ class ExpoIapModule : Module() {
441446
}
442447
}
443448

449+
@Suppress("UNCHECKED_CAST")
444450
AsyncFunction("verifyPurchase") { params: Map<String, Any?>, promise: Promise ->
445451
ExpoIapLog.payload("verifyPurchase", params)
446452
scope.launch {
@@ -639,6 +645,7 @@ class ExpoIapModule : Module() {
639645
"external-offer" -> OpenIapBillingProgram.ExternalOffer
640646
"external-content-link" -> OpenIapBillingProgram.ExternalContentLink
641647
"external-payments" -> OpenIapBillingProgram.ExternalPayments
648+
"user-choice-billing" -> OpenIapBillingProgram.UserChoiceBilling
642649
else -> OpenIapBillingProgram.Unspecified
643650
}
644651

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
slug: v3.4.0-purchasestate-cleanup
3+
title: v3.4.0 - PurchaseState Cleanup & API Consolidation
4+
authors: [hyochan]
5+
tags: [release, openiap, android, billing, breaking-change]
6+
---
7+
8+
## v3.4.0 Release Notes
9+
10+
This release reflects [OpenIAP v1.3.11 updates](https://www.openiap.dev/docs/updates/notes#gql-1-3-11-google-1-3-21-apple-1-3-9), simplifying the `PurchaseState` enum and consolidating the Android billing API.
11+
12+
### Breaking Changes
13+
14+
- **PurchaseState**: Removed `failed`, `restored`, `deferred` (now only `pending`, `purchased`, `unknown`)
15+
- **AlternativeBillingModeAndroid**: Deprecated in favor of `BillingProgramAndroid`
16+
- **useAlternativeBilling**: Deprecated (only logged debug info, had no effect on purchase flow)
17+
18+
### Migration Guide
19+
20+
| Before (Deprecated) | After (Recommended) |
21+
|---------------------|---------------------|
22+
| `alternativeBillingModeAndroid: 'user-choice'` | `enableBillingProgramAndroid: 'user-choice-billing'` |
23+
| `alternativeBillingModeAndroid: 'alternative-only'` | `enableBillingProgramAndroid: 'external-offer'` |
24+
25+
```typescript
26+
// Before
27+
const {connected} = useIAP({
28+
alternativeBillingModeAndroid: 'user-choice',
29+
});
30+
31+
// After
32+
const {connected} = useIAP({
33+
enableBillingProgramAndroid: 'user-choice-billing',
34+
});
35+
```
36+
37+
### OpenIAP Versions
38+
39+
| Package | Version |
40+
|---------|---------|
41+
| openiap-gql | 1.3.11 |
42+
| openiap-google | 1.3.21 |
43+
| openiap-apple | 1.3.9 |
44+
45+
For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#gql-1-3-11-google-1-3-21-apple-1-3-9).

docs/docs/api/error-codes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ React Native IAP provides a centralized error handling system with platform-spec
1717
The `ErrorCode` enum provides standardized error codes that map to platform-specific errors:
1818

1919
```tsx
20-
import {ErrorCode} from 'react-native-iap';
20+
import {ErrorCode} from 'expo-iap';
2121

2222
export enum ErrorCode {
2323
Unknown = 'unknown',

docs/docs/api/error-handling.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ import {
1919
isNetworkError,
2020
getUserFriendlyErrorMessage,
2121
ErrorCode,
22-
} from 'react-native-iap';
22+
} from 'expo-iap';
2323

2424
try {
25-
await requestPurchase({request: {sku: 'product_id'}});
25+
await requestPurchase({
26+
request: {
27+
apple: {sku: 'product_id'},
28+
google: {skus: ['product_id']},
29+
},
30+
});
2631
} catch (error) {
2732
// Check for user cancellation
2833
if (isUserCancelledError(error)) {
@@ -68,7 +73,7 @@ interface PurchaseError extends Error {
6873
Use the `ErrorCode` enum for type-safe error code comparisons:
6974

7075
```ts
71-
import {ErrorCode} from 'react-native-iap';
76+
import {ErrorCode} from 'expo-iap';
7277

7378
if (error instanceof PurchaseError && error.code === ErrorCode.UserCancelled) {
7479
// Handle user cancellation

docs/docs/api/methods/unified-apis.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const buySubscription = (subscriptionId: string, subscription?: any) => {
154154
google: {
155155
skus: [subscriptionId],
156156
subscriptionOffers:
157-
subscription?.subscriptionOfferDetails?.map((offer) => ({
157+
subscription?.subscriptionOfferDetailsAndroid?.map((offer) => ({
158158
sku: subscriptionId,
159159
offerToken: offer.offerToken,
160160
})) || [],
@@ -175,9 +175,11 @@ const buySubscription = (subscriptionId: string, subscription?: any) => {
175175
```tsx
176176
await requestPurchase({
177177
request: {
178-
sku: productId,
179-
quantity: 1,
180-
appAccountToken: 'user-account-token',
178+
apple: {
179+
sku: productId,
180+
quantity: 1,
181+
appAccountToken: 'user-account-token',
182+
},
181183
},
182184
type: 'in-app',
183185
});
@@ -693,7 +695,7 @@ interface Purchase {
693695
// Android-specific properties
694696
dataAndroid?: string;
695697
signatureAndroid?: string;
696-
purchaseStateAndroid?: number;
698+
purchaseState?: 'pending' | 'purchased' | 'unknown';
697699
isAcknowledgedAndroid?: boolean;
698700
packageNameAndroid?: string;
699701
developerPayloadAndroid?: string;

docs/docs/api/types.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ export type IapStore = 'unknown' | 'apple' | 'google' | 'horizon';
2424
export type ProductType = 'in-app' | 'subs';
2525

2626
export type PurchaseState =
27-
| 'deferred'
28-
| 'failed'
2927
| 'pending'
3028
| 'purchased'
31-
| 'restored'
3229
| 'unknown';
30+
31+
// Note: 'failed', 'restored', and 'deferred' were removed in v3.4.0
32+
// - Failed: Both platforms return errors instead of Purchase objects on failure
33+
// - Restored: Restored purchases return as 'purchased' state
34+
// - Deferred: iOS StoreKit 2 has no transaction state; Android uses 'pending'
3335
```
3436

3537
The `ErrorCode` enum now mirrors the OpenIAP schema without the legacy `E_` prefix:
@@ -214,7 +216,13 @@ New types for the Google Play Billing Programs API:
214216

215217
```ts
216218
// Billing program types
217-
type BillingProgramAndroid = 'unspecified' | 'external-content-link' | 'external-offer';
219+
// USER_CHOICE_BILLING added in v3.4.0 (available in Google Play Billing 7.0+)
220+
type BillingProgramAndroid =
221+
| 'unspecified'
222+
| 'external-content-link'
223+
| 'external-offer'
224+
| 'external-payments'
225+
| 'user-choice-billing';
218226

219227
// Launch mode for external links
220228
type ExternalLinkLaunchModeAndroid =

docs/docs/api/use-iap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ const buySubscriptionWithOffer = async (
371371
try {
372372
// Updates `availablePurchases` state; do not expect a return value
373373
await getAvailablePurchases();
374-
// Read from state afterwards
374+
// Read from state afterward
375375
console.log('Available purchases count:', availablePurchases.length);
376376
} catch (error) {
377377
console.error('Failed to fetch available purchases:', error);

docs/docs/examples/subscription-flow.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ function useSubscriptionStatus() {
200200

201201
// Platform-specific status checks
202202
if (Platform.OS === 'ios') {
203-
// iOS provides expirationDateIos
204-
const isExpired = subscription.expirationDateIos < Date.now();
203+
// iOS provides expirationDateIOS
204+
const isExpired = subscription.expirationDateIOS < Date.now();
205205
setSubscriptionDetails({
206206
productId: subscription.productId,
207207
isActive: !isExpired,
208-
expiresAt: new Date(subscription.expirationDateIos),
208+
expiresAt: new Date(subscription.expirationDateIOS),
209209
environment: subscription.environmentIOS, // 'Production' or 'Sandbox'
210210
});
211211
} else {
@@ -214,7 +214,7 @@ function useSubscriptionStatus() {
214214
productId: subscription.productId,
215215
isActive: subscription.autoRenewingAndroid,
216216
willAutoRenew: subscription.autoRenewingAndroid,
217-
purchaseState: subscription.purchaseStateAndroid, // 0 = purchased, 1 = canceled
217+
purchaseState: subscription.purchaseState, // 'pending' | 'purchased' | 'unknown'
218218
});
219219
}
220220
} else {
@@ -281,7 +281,7 @@ async function getUserSubscriptionTier() {
281281
**Android:**
282282

283283
- `autoRenewingAndroid`: Boolean for auto-renewal status
284-
- `purchaseStateAndroid`: Purchase state (0 = purchased, 1 = canceled)
284+
- `purchaseState`: Purchase state ('pending' | 'purchased' | 'unknown')
285285

286286
⚠️ **Always validate on your server.** Client-side checks are for UI only.
287287

@@ -656,7 +656,7 @@ function SubscriptionPlanManager() {
656656
| **Parameters** | Just new `sku` | `purchaseTokenAndroid` + `replacementModeAndroid` |
657657
| **Timing** | OS-determined | Specified via `replacementModeAndroid` |
658658
| **Plan Changes** | Use subscription groups with ranks | Use base plans and offers |
659-
| **Status Check** | Check `expirationDateIos` | Check `autoRenewingAndroid` |
659+
| **Status Check** | Check `expirationDateIOS` | Check `autoRenewingAndroid` |
660660
| **Cancellation Detection** | User manages in Settings | Check `autoRenewingAndroid === false` |
661661
| **Proration** | Handled by App Store | Configurable via `replacementModeAndroid` |
662662

docs/docs/getting-started/setup-android.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ const AndroidProductItem = ({product}: {product: Product}) => {
9191
const handlePurchase = () => {
9292
if (product.platform === 'android') {
9393
requestPurchase({
94-
request: {skus: [product.id]},
94+
request: {
95+
google: {skus: [product.id]},
96+
},
9597
type: 'in-app',
9698
});
9799
}
@@ -142,7 +144,7 @@ const AndroidSubscriptionItem = ({
142144
return (
143145
<View>
144146
<Text>{subscription.title}</Text>
145-
{subscription.subscriptionOfferDetails?.map((offer) => (
147+
{subscription.subscriptionOfferDetailsAndroid?.map((offer) => (
146148
<TouchableOpacity
147149
key={offer.offerId}
148150
onPress={() => handleSubscribe(offer)}

0 commit comments

Comments
 (0)