Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .claude/commands/verify-all.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ set -euo pipefail
COMPILER_INDEX_STORE_ENABLE=NO)

# Expo bridge tests and clean native prebuilds/consumer builds
(cd libraries/expo-iap && bun run lint:tsc && bun run test)
(cd libraries/expo-iap && bun run build:plugin && bun run lint:tsc && bun run test)
(cd libraries/expo-iap/example && bun run test -- --runInBand)
(cd libraries/expo-iap/example && \
npx expo prebuild --platform android --clean)
Expand All @@ -92,7 +92,13 @@ set -euo pipefail
([ -f env ] || cp env.example env) && \
flutter build apk --debug)
(cd libraries/flutter_inapp_purchase/example/ios && \
pod install --repo-update && \
pod install --repo-update)
# CocoaPods regenerates FlutterGeneratedPluginSwiftPackage at Flutter's iOS 13
# default. Run Flutter's configuration step afterward so it raises that
# aggregate package to the app/plugin iOS 15 deployment target.
(cd libraries/flutter_inapp_purchase/example && \
flutter build ios --config-only --simulator)
(cd libraries/flutter_inapp_purchase/example/ios && \
xcodebuild build \
-workspace Runner.xcworkspace \
-scheme Runner \
Expand Down
113 changes: 71 additions & 42 deletions knowledge/_claude-context/context.md

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions knowledge/external/google-billing-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,16 @@ if (purchase.isSuspended) {
### Query Suspended Subscriptions (8.1+)

```kotlin
// Include suspended subscriptions in query results
val params = QueryPurchasesParams.newBuilder()
// Include suspended subscriptions when the connected Play Store supports it.
val paramsBuilder = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.setIncludeSuspended(true) // New in 8.1
.build()
if (billingClient.isFeatureSupported(
BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS
).responseCode == BillingClient.BillingResponseCode.OK
) {
paramsBuilder.includeSuspendedSubscriptions(true) // New in 8.1
}
val params = paramsBuilder.build()

billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
purchases.forEach { purchase ->
Expand Down
4 changes: 4 additions & 0 deletions knowledge/external/horizon-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ import com.meta.horizon.billingclient.api.*

### Important Notes

- The Billing Compatibility SDK initializes Horizon platform state from an
Android `Activity`. OpenIAP therefore requires a current foreground Activity
for Horizon `initConnection`; it returns `MissingCurrentActivity` instead of
falling back to an application context.
- Horizon Billing Compatibility 2.x reads the app id from Android manifest
meta-data key `com.meta.horizon.platform.HORIZON_APP_ID`. The older
`com.meta.horizon.platform.ovr.OCULUS_APP_ID` key is deprecated; OpenIAP also
Expand Down
94 changes: 57 additions & 37 deletions knowledge/external/storekit2-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,34 @@ This document provides external API reference for Apple's StoreKit 2 framework.

## Recent StoreKit Features

| Feature | iOS Version | Description |
| -------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
| Win-back offers | iOS 18.0 | Re-engage churned subscribers |
| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase |
| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key |
| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) |
| UI context for purchases | iOS 18.2 | Required for proper payment sheet display |
| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` |
| `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) |
| `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) |
| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction |
| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction |
| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction |
| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables |
| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format |
| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option |
| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` |
| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions |
| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata |
| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data |
| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp |
| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation |
| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform |
| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload |
| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API |
| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning |
| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers |
| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 |
| Feature | iOS Version | Description |
| -------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Win-back offers | iOS 18.0 | Re-engage churned subscribers |
| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase |
| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key |
| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) |
| UI context for purchases | iOS 18.2 | Required for proper payment sheet display |
| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` |
| `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) |
| `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) |
| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction |
| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction |
| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction |
| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables |
| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format |
| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option |
| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` |
| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions |
| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata |
| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data |
| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp |
| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation |
| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform |
| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload |
| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API |
| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning |
| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers |
| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 |

### StoreKit Message presentation

Expand Down Expand Up @@ -77,16 +77,21 @@ let result = try await AppStore.presentOfferCodeRedeemSheet(

SwiftUI exposes the same result through
`offerCodeRedemption(options:isPresented:onCompletion:)`. These APIs require the
Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose only the
legacy redemption sheet API.
Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose the StoreKit 2
scene-based `AppStore.presentOfferCodeRedeemSheet(in:)` API, which presents the
sheet but does not return the redeemed transaction.

OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`.
Xcode 27 builds call the new API, require a verified result, and return the
mapped transaction. Xcode 26 builds retain the legacy sheet; iOS and Mac
Catalyst 14–26 therefore return `nil` after presentation and rely on the
mapped transaction on Apple 27+ runtimes. Older result paths use the StoreKit 2
scene API on iOS 16+ and visionOS 1+ and return `nil` after presentation; iOS 15
retains the StoreKit 1 fallback. In Mac Catalyst apps, the scene API throws
`StoreKitError.unknown`, while the Catalyst 15 StoreKit 1 call has no effect and
returns `nil`. Nil results from an actually presented sheet rely on the
transaction listener or explicit purchase reconciliation. Xcode 27 beta 4
declares `RedeemOption`, but its public symbol graph exposes no constructible
option values, so OpenIAP currently passes an empty set.
declares `RedeemOption`,
but its public symbol graph exposes no constructible option values, so OpenIAP
currently passes an empty set.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Subscription Bundles and Suites (Xcode 27 beta)

Expand Down Expand Up @@ -369,17 +374,28 @@ let result = try await product.purchase(confirmIn: window)

> **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene.

## AppTransaction Updates (iOS 18.4+)
## AppTransaction Identity Updates (Xcode 16.4+; back-deployed)

```swift
let appTransaction = try await AppTransaction.shared

// New in iOS 18.4 (back-deployed to iOS 15)
// Introduced in iOS 18.4 (back-deployed to the AppTransaction baseline)
let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account
let originalPlatform = appTransaction.originalPlatform // Original purchase platform
let originalPlatform = appTransaction.originalPlatform // Typed value on iOS 18.4+
```

OpenIAP uses `originalPlatformStringRepresentation` on older runtimes. The typed
`originalPlatform` property starts at iOS 18.4, macOS 15.4, tvOS 18.4, watchOS
11.4, and visionOS 2.4.

## AppTransaction Acquisition Updates (Xcode 27 SDK)

```swift
let appTransaction = try await AppTransaction.shared

// Public in the Xcode 27 SDK and back-deployed to these existing runtimes
let revocationDate = appTransaction.revocationDate // App-acquisition revocation
// Runtime-gated to Apple 27+
let storeType = appTransaction.storeType // Acquisition store channel
```

Expand Down Expand Up @@ -425,6 +441,10 @@ if let advancedInfo = product.advancedCommerceInfo {
}
```

For Advanced Commerce transactions, OpenIAP maps
`AdvancedCommerceInfoIOS.period` as an optional `SubscriptionPeriodValueIOS`
containing the subscription period `unit` and integer `value`.

## Monthly Subscriptions With 12-Month Commitment (iOS 26.4+)

This billing plan lets customers pay monthly while committing to an annual
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class ExpoIapModule : Module() {
ExpoIapLog.debug("Activity available: ${it.javaClass.name}")
openIap.setActivity(it)
}.onFailure {
ExpoIapLog.warning("Activity not available during initConnection - OpenIAP will use Context")
ExpoIapLog.warning("Activity not available during initConnection")
}

// If already connected, short-circuit
Expand Down
4 changes: 2 additions & 2 deletions libraries/expo-iap/example/__tests__/offer-code.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('OfferCode Component', () => {
});
});

it('should explain the legacy iOS redemption result', async () => {
it('should explain a nil iOS redemption result', async () => {
Object.defineProperty(Platform, 'OS', {
get: jest.fn(() => 'ios'),
configurable: true,
Expand All @@ -146,7 +146,7 @@ describe('OfferCode Component', () => {
await waitFor(() => {
expect(Alert.alert).toHaveBeenCalledWith(
'Redemption Sheet Presented',
'This iOS version uses the legacy sheet. Refresh available purchases after completing redemption.',
'The system sheet did not return a transaction directly. Refresh available purchases after completing redemption.',
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion libraries/expo-iap/example/app/offer-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function OfferCodeScreen() {
} else {
Alert.alert(
'Redemption Sheet Presented',
'This iOS version uses the legacy sheet. Refresh available purchases after completing redemption.',
'The system sheet did not return a transaction directly. Refresh available purchases after completing redemption.',
);
}
} else {
Expand Down
Loading