diff --git a/libraries/expo-iap/ios/ExpoIapModule.swift b/libraries/expo-iap/ios/ExpoIapModule.swift index 3c803104..c220f614 100644 --- a/libraries/expo-iap/ios/ExpoIapModule.swift +++ b/libraries/expo-iap/ios/ExpoIapModule.swift @@ -148,6 +148,14 @@ public final class ExpoIapModule: Module { return sanitized } + AsyncFunction("getAllTransactionsIOS") { () async throws -> [[String: Any]] in + ExpoIapLog.payload("getAllTransactionsIOS", payload: nil) + let all = try await OpenIapModule.shared.getAllTransactionsIOS() + let sanitized = all.map { ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) } + ExpoIapLog.result("getAllTransactionsIOS", value: sanitized) + return sanitized + } + AsyncFunction("clearTransactionIOS") { () async throws -> Bool in ExpoIapLog.payload("clearTransactionIOS", payload: nil) let success = try await OpenIapModule.shared.clearTransactionIOS() diff --git a/libraries/expo-iap/src/modules/ios.ts b/libraries/expo-iap/src/modules/ios.ts index eb8acb53..55a30cc8 100644 --- a/libraries/expo-iap/src/modules/ios.ts +++ b/libraries/expo-iap/src/modules/ios.ts @@ -357,6 +357,13 @@ export const getPendingTransactionsIOS: QueryField< return (transactions ?? []) as PurchaseIOS[]; }; +export const getAllTransactionsIOS: QueryField< + 'getAllTransactionsIOS' +> = async () => { + const transactions = await ExpoIapModule.getAllTransactionsIOS(); + return (transactions ?? []) as PurchaseIOS[]; +}; + /** * Clear a specific transaction (iOS only). * diff --git a/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift b/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift index 1057813f..bbcef3da 100644 --- a/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift +++ b/libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift @@ -142,6 +142,9 @@ public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin { case "getPendingTransactionsIOS": getPendingTransactionsIOS(result: result) + case "getAllTransactionsIOS": + getAllTransactionsIOS(result: result) + case "requestPurchaseOnPromotedProductIOS": requestPurchaseOnPromotedProductIOS(result: result) @@ -663,6 +666,23 @@ public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin { } } + private func getAllTransactionsIOS(result: @escaping FlutterResult) { + Task { @MainActor in + do { + let all = try await OpenIapModule.shared.getAllTransactionsIOS() + let purchases = all.map { Purchase.purchaseIos($0) } + let serialized = FlutterIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases)) + FlutterIapLog.result("getAllTransactionsIOS", value: serialized) + result(serialized) + } catch { + await MainActor.run { + let code: ErrorCode = .serviceError + result(FlutterError(code: code.rawValue, message: defaultMessage(for: code), details: nil)) + } + } + } + } + private func clearTransactionIOS(result: @escaping FlutterResult) { FlutterIapLog.debug("clearTransactionIOS called") Task { @MainActor in diff --git a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart index f0c5f690..1200f520 100644 --- a/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart +++ b/libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart @@ -964,6 +964,26 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { return const []; }; + gentype.QueryGetAllTransactionsIOSHandler get getAllTransactionsIOS => + () async { + if (_platform.isIOS || _platform.isMacOS) { + final dynamic result = await _channel.invokeMethod( + 'getAllTransactionsIOS', + ); + final purchases = extractPurchases( + result, + platformIsAndroid: _platform.isAndroid, + platformIsIOS: _platform.isIOS || _platform.isMacOS, + acknowledgedAndroidPurchaseTokens: + _acknowledgedAndroidPurchaseTokens, + ); + return purchases.whereType().toList( + growable: false, + ); + } + return const []; + }; + gentype.MutationAcknowledgePurchaseAndroidHandler get acknowledgePurchaseAndroid => (purchaseToken) async { if (!_platform.isAndroid) { @@ -2254,6 +2274,7 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi { getAvailablePurchases: getAvailablePurchases, getExternalPurchaseCustomLinkTokenIOS: getExternalPurchaseCustomLinkTokenIOS, + getAllTransactionsIOS: getAllTransactionsIOS, getPendingTransactionsIOS: getPendingTransactionsIOS, getPromotedProductIOS: getPromotedProductIOS, getStorefront: getStorefront, diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 7f1fb6c1..e0bb9c57 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -641,6 +641,23 @@ func get_pending_transactions_ios() -> Array: purchases.append(Types.PurchaseIOS.from_dict(tx)) return purchases +## Get all transactions including finished consumables (iOS only). +## Requires SK2ConsumableTransactionHistory Info.plist key for finished consumables (iOS 18+). +## @return Array of Types.PurchaseIOS +func get_all_transactions_ios() -> Array: + var purchases: Array = [] + if _native_plugin and _platform == "iOS": + var result_json = _native_plugin.call("getAllTransactionsIOS") + var result = JSON.parse_string(result_json) + if result is Dictionary and result.get("success", false): + var transactions_json = result.get("transactionsJson", "[]") + var transactions = JSON.parse_string(transactions_json) + if transactions is Array: + for tx in transactions: + if tx is Dictionary: + purchases.append(Types.PurchaseIOS.from_dict(tx)) + return purchases + ## Present code redemption sheet (iOS only). ## @return Types.VoidResult func present_code_redemption_sheet_ios() -> Variant: diff --git a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift index 15d9e426..ee5174ac 100644 --- a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift +++ b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift @@ -483,6 +483,33 @@ public class GodotIap: RefCounted, @unchecked Sendable { return "{\"status\": \"pending\"}" } + @Callable + public func getAllTransactionsIOS() -> String { + GodotIapLog.payload("Getting all transactions", payload: nil) + + Task { [weak self] in + guard let self = self else { return } + do { + let transactions = try await self.openIap.getAllTransactionsIOS() + let transactionDicts = transactions.map { self.purchaseIOSToDictionary($0) } + + if let jsonData = try? JSONSerialization.data(withJSONObject: transactionDicts), + let jsonString = String(data: jsonData, encoding: .utf8) { + await MainActor.run { [self] in + let dict = VariantDictionary() + dict["success"] = Variant(true) + dict["transactionsJson"] = Variant(jsonString) + self.productsFetched.emit(dict) + } + } + } catch { + GodotIapLog.debug("[GodotIap] getAllTransactionsIOS error: \(error.localizedDescription)") + } + } + + return "{\"status\": \"pending\"}" + } + @Callable public func presentCodeRedemptionSheetIOS() -> String { GodotIapLog.payload("Presenting code redemption sheet", payload: nil) diff --git a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index ee38ea48..34428bae 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -1546,6 +1546,12 @@ class HybridRnIap : HybridRnIapSpec() { } } + override fun getAllTransactionsIOS(): Promise> { + return Promise.async { + throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported())) + } + } + override fun syncIOS(): Promise { return Promise.async { throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported())) diff --git a/libraries/react-native-iap/ios/HybridRnIap.swift b/libraries/react-native-iap/ios/HybridRnIap.swift index 36c9b10f..a1677c22 100644 --- a/libraries/react-native-iap/ios/HybridRnIap.swift +++ b/libraries/react-native-iap/ios/HybridRnIap.swift @@ -717,6 +717,39 @@ class HybridRnIap: HybridRnIapSpec { } } + func getAllTransactionsIOS() throws -> Promise<[NitroPurchase]> { + return Promise.async { + do { + RnIapLog.payload("getAllTransactionsIOS", nil) + let all = try await OpenIapModule.shared.getAllTransactionsIOS() + var unionPurchases: [OpenIAP.Purchase] = [] + var payloadUpdates: [String: [String: Any]] = [:] + for purchase in all { + let union = OpenIAP.Purchase.purchaseIos(purchase) + unionPurchases.append(union) + let raw = OpenIapSerialization.purchase(union) + if let identifier = raw["id"] as? String { + payloadUpdates[identifier] = raw + } + } + await MainActor.run { + for (key, value) in payloadUpdates { + self.purchasePayloadById[key] = value + } + } + let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(unionPurchases)) + RnIapLog.result("getAllTransactionsIOS", payloads) + return payloads.map { RnIapHelper.convertPurchaseDictionary($0) } + } catch let purchaseError as PurchaseError { + RnIapLog.failure("getAllTransactionsIOS", error: purchaseError) + throw OpenIapException(purchaseError.toJsonString()) + } catch { + RnIapLog.failure("getAllTransactionsIOS", error: error) + throw OpenIapException(toErrorJson(OpenIAPError.ServiceError(debugMessage: error.localizedDescription))) + } + } + } + func syncIOS() throws -> Promise { return Promise.async { do { diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts index cad3ebeb..8771602c 100644 --- a/libraries/react-native-iap/src/__tests__/index.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.test.ts @@ -38,6 +38,7 @@ const mockIap: any = { requestPromotedProductIOS: jest.fn(async () => null), buyPromotedProductIOS: jest.fn(async () => undefined), presentCodeRedemptionSheetIOS: jest.fn(async () => true), + getAllTransactionsIOS: jest.fn(async () => []), // Unified storefront getStorefront: jest.fn(async () => 'USA'), diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index fe7a8e06..c5fc07dc 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -1080,6 +1080,32 @@ export const getPendingTransactionsIOS: QueryField< } }; +export const getAllTransactionsIOS: QueryField< + 'getAllTransactionsIOS' +> = async () => { + if (Platform.OS !== 'ios') { + return []; + } + + try { + const nitroPurchases = await IAP.instance.getAllTransactionsIOS(); + return nitroPurchases + .map(convertNitroPurchaseToPurchase) + .filter( + (purchase): purchase is PurchaseIOS => purchase.platform === 'ios', + ); + } catch (error) { + RnIapConsole.error('[getAllTransactionsIOS] Failed:', error); + const parsedError = parseErrorStringToJsonObj(error); + throw createPurchaseError({ + code: parsedError.code, + message: parsedError.message, + responseCode: parsedError.responseCode, + debugMessage: parsedError.debugMessage, + }); + } +}; + export const showManageSubscriptionsIOS: MutationField< 'showManageSubscriptionsIOS' > = async () => { diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts index 2c90288f..94643735 100644 --- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts +++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts @@ -857,6 +857,14 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> { */ getPendingTransactionsIOS(): Promise; + /** + * Get the full StoreKit 2 transaction history as PurchaseIOS values. + * Requires SK2ConsumableTransactionHistory Info.plist key for finished consumables (iOS 18+). + * @returns Promise - Array of all transactions + * @platform iOS + */ + getAllTransactionsIOS(): Promise; + /** * Sync with the App Store (iOS only) * @returns Promise - Success flag diff --git a/packages/docs/src/pages/docs/foundation/founding-supporters.tsx b/packages/docs/src/pages/docs/foundation/founding-supporters.tsx index 78001aed..7b458b69 100644 --- a/packages/docs/src/pages/docs/foundation/founding-supporters.tsx +++ b/packages/docs/src/pages/docs/foundation/founding-supporters.tsx @@ -15,6 +15,21 @@ function FoundingSupporters() { keywords="OpenIAP founding supporter, open source sponsor, IAP standard supporter" />

Become a Founding Supporter

+
+ Draft — The Foundation section is currently being + prepared. Content may change as the governance structure is finalized. +

We're building OpenIAP into a vendor-neutral, open standard for in-app purchases — and we're looking for organizations to join as Founding diff --git a/packages/docs/src/pages/docs/foundation/governance.tsx b/packages/docs/src/pages/docs/foundation/governance.tsx index f59b2555..33420104 100644 --- a/packages/docs/src/pages/docs/foundation/governance.tsx +++ b/packages/docs/src/pages/docs/foundation/governance.tsx @@ -14,6 +14,21 @@ function Governance() { keywords="OpenIAP governance, open source governance, TSC, technical steering committee, maintainer policy" />

Project Governance

+
+ Draft — The Foundation section is currently being + prepared. Content may change as the governance structure is finalized. +

OpenIAP is an open-source project providing a neutral interoperability standard for in-app purchase APIs and verification across platforms. diff --git a/packages/docs/src/pages/docs/foundation/one-pager.tsx b/packages/docs/src/pages/docs/foundation/one-pager.tsx index 40c01cdd..9ef12f75 100644 --- a/packages/docs/src/pages/docs/foundation/one-pager.tsx +++ b/packages/docs/src/pages/docs/foundation/one-pager.tsx @@ -17,6 +17,21 @@ function OnePager() { OpenIAP: Neutral Interoperability Layer for In-App Purchase APIs and Verification +

+ Draft — The Foundation section is currently being + prepared. Content may change as the governance structure is finalized. +
@@ -27,7 +42,85 @@ function OnePager() { Every framework — React Native, Expo, Flutter, KMP, Godot, native iOS, native Android — reinvents the same wheel: different type definitions, different error models, different verification flows, different - edge-case handling. This leads to: + edge-case handling. +

+

+ The landscape is expanding rapidly. New platforms like{' '} + + Meta Horizon OS + + ,{' '} + + Vega OS + + ,{' '} + + HarmonyOS + + , and{' '} + + Amazon Fire OS + {' '} + continue to emerge and grow, while stores beyond Google Play and the + App Store — such as{' '} + + Galaxy Store + + ,{' '} + + Huawei AppGallery + + , and alternative marketplaces like{' '} + + Onside + {' '} + — each bring their own billing APIs. Even the established stores like{' '} + + Google Play + {' '} + and{' '} + + Apple App Store + {' '} + evolve their billing APIs with every major release. As this + fragmentation accelerates with every new platform and marketplace, a + unified standard becomes not just useful but essential. This leads to:

  • @@ -43,8 +136,8 @@ function OnePager() { prevention are left as afterthoughts
  • - High maintenance burden as Apple and Google - frequently change their billing APIs + High maintenance burden as Apple, Google, and an + expanding set of stores frequently change their billing APIs
diff --git a/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx b/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx index 0a6da038..4c04ab29 100644 --- a/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx +++ b/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx @@ -14,6 +14,21 @@ function RoadmapBudget() { keywords="OpenIAP roadmap, funding plan, open source budget, IAP development roadmap" />

Roadmap & Budget

+
+ Draft — The Foundation section is currently being + prepared. Content may change as the governance structure is finalized. +

This document outlines how OpenIAP plans to grow and how sponsorship funding is allocated. Full transparency on where every dollar goes. @@ -263,7 +278,7 @@ function RoadmapBudget() { Domain registration - ~$2 + ~$3 openiap.dev annual amortized @@ -281,12 +296,22 @@ function RoadmapBudget() { ~$2 $25 one-time amortized + + Claude Code (AI assistant) + $200 + Anthropic Max plan for development + + + Codex (AI assistant) + $100 + OpenAI for code review and testing + Total baseline - ~$80/month + $330–380/month Excluding maintainer time diff --git a/packages/docs/src/pages/docs/foundation/sponsorship.tsx b/packages/docs/src/pages/docs/foundation/sponsorship.tsx index 1b972c91..7dc1c6f0 100644 --- a/packages/docs/src/pages/docs/foundation/sponsorship.tsx +++ b/packages/docs/src/pages/docs/foundation/sponsorship.tsx @@ -15,6 +15,21 @@ function Sponsorship() { keywords="OpenIAP sponsorship, open source funding, IAP sponsor, founding supporter" />

Sponsorship

+
+ Draft — The Foundation section is currently being + prepared. Content may change as the governance structure is finalized. +

OpenIAP is the open interoperability standard for in-app purchases. Your sponsorship directly funds the infrastructure, security, and diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 98a18152..3d4327b8 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -152,6 +152,51 @@ function Releases() { openiap-apple 2.1.2 +

  • + + react-native-iap 15.2.1 + +
  • +
  • + + expo-iap 4.2.1 + +
  • +
  • + + flutter_inapp_purchase 9.2.1 + +
  • +
  • + + kmp-iap 2.2.1 + +
  • +
  • + + godot-iap 2.2.1 + +
  • diff --git a/packages/docs/src/pages/docs/updates/versions.tsx b/packages/docs/src/pages/docs/updates/versions.tsx index d21ff6de..fd0ffcb7 100644 --- a/packages/docs/src/pages/docs/updates/versions.tsx +++ b/packages/docs/src/pages/docs/updates/versions.tsx @@ -8,7 +8,7 @@ const GOOGLE_MAVEN_BADGE = const GOOGLE_MAVEN_ARTIFACT = 'https://central.sonatype.com/artifact/io.github.hyochan.openiap/openiap-google'; const APPLE_SWIFT_BADGE = - 'https://img.shields.io/github/v/tag/hyodotdev/openiap?filter=apple-v*&label=Swift%20Package&logo=swift&color=orange'; + 'https://img.shields.io/github/v/tag/hyodotdev/openiap?filter=2.*&label=Swift%20Package&logo=swift&color=orange'; const APPLE_SWIFT_URL = 'https://github.com/hyodotdev/openiap/tree/main/packages/apple'; const APPLE_COCOAPODS_BADGE = @@ -153,24 +153,15 @@ function Versions() { href={latestGqlRelease.pageUrl} target="_blank" rel="noopener noreferrer" - className="btn btn-secondary" + className="btn btn-secondary no-icon" > - Latest tag: {latestGqlRelease.tag} + Latest tag: {latestGqlRelease.tag} ↗ View all releases diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 3107a654..68fb652d 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -553,20 +553,20 @@ } /* Documentation Links - Oatmeal Colors */ -.doc-page a:not(.anchor-link) { +.doc-page a:not(.anchor-link, .btn) { color: #8b6545; /* Warm brown */ text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s ease; } -.doc-page a:not(.anchor-link):hover { +.doc-page a:not(.anchor-link, .btn):hover { border-bottom-color: #d4a574; /* Light oatmeal */ } /* External links with icon */ .doc-page a.external-link, -.doc-page a[target='_blank'] { +.doc-page a[target='_blank']:not(.btn) { color: #8b6545; text-decoration: none; border-bottom: 1px dotted #d4a574; @@ -577,13 +577,13 @@ } .doc-page a.external-link:hover, -.doc-page a[target='_blank']:hover { +.doc-page a[target='_blank']:not(.btn):hover { color: var(--primary-dark); border-bottom-style: solid; } .doc-page a.external-link::after, -.doc-page a[target='_blank']:not(.no-icon)::after { +.doc-page a[target='_blank']:not(.no-icon, .btn)::after { content: '↗'; font-size: 0.75em; opacity: 0.7; @@ -591,7 +591,7 @@ } .doc-page a.external-link:hover::after, -.doc-page a[target='_blank']:hover::after { +.doc-page a[target='_blank']:not(.btn):hover::after { transform: translate(2px, -2px); opacity: 1; } @@ -639,23 +639,23 @@ } /* Dark mode adjustments for links - Oatmeal theme */ -:root.dark .doc-page a:not(.anchor-link) { +:root.dark .doc-page a:not(.anchor-link, .btn) { color: #d4a574; } -:root.dark .doc-page a:not(.anchor-link):hover { +:root.dark .doc-page a:not(.anchor-link, .btn):hover { color: #e6c299; border-bottom-color: #d4a574; } :root.dark .doc-page a.external-link, -:root.dark .doc-page a[target='_blank'] { +:root.dark .doc-page a[target='_blank']:not(.btn) { color: #d4a574; border-bottom-color: rgba(212, 165, 116, 0.3); } :root.dark .doc-page a.external-link:hover, -:root.dark .doc-page a[target='_blank']:hover { +:root.dark .doc-page a[target='_blank']:not(.btn):hover { color: #e6c299; border-bottom-color: rgba(212, 165, 116, 0.6); }