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
8 changes: 8 additions & 0 deletions libraries/expo-iap/ios/ExpoIapModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions libraries/expo-iap/src/modules/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin {
case "getPendingTransactionsIOS":
getPendingTransactionsIOS(result: result)

case "getAllTransactionsIOS":
getAllTransactionsIOS(result: result)

case "requestPurchaseOnPromotedProductIOS":
requestPurchaseOnPromotedProductIOS(result: result)

Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,26 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
return const <gentype.PurchaseIOS>[];
};

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<gentype.PurchaseIOS>().toList(
growable: false,
);
}
return const <gentype.PurchaseIOS>[];
};

gentype.MutationAcknowledgePurchaseAndroidHandler
get acknowledgePurchaseAndroid => (purchaseToken) async {
if (!_platform.isAndroid) {
Expand Down Expand Up @@ -2254,6 +2274,7 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
getAvailablePurchases: getAvailablePurchases,
getExternalPurchaseCustomLinkTokenIOS:
getExternalPurchaseCustomLinkTokenIOS,
getAllTransactionsIOS: getAllTransactionsIOS,
getPendingTransactionsIOS: getPendingTransactionsIOS,
getPromotedProductIOS: getPromotedProductIOS,
getStorefront: getStorefront,
Expand Down
17 changes: 17 additions & 0 deletions libraries/godot-iap/addons/godot-iap/godot_iap.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,12 @@ class HybridRnIap : HybridRnIapSpec() {
}
}

override fun getAllTransactionsIOS(): Promise<Array<NitroPurchase>> {
return Promise.async {
throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
}
}

override fun syncIOS(): Promise<Boolean> {
return Promise.async {
throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
Expand Down
33 changes: 33 additions & 0 deletions libraries/react-native-iap/ios/HybridRnIap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Comment thread
hyochan marked this conversation as resolved.
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)))
}
Comment thread
hyochan marked this conversation as resolved.
}
}

func syncIOS() throws -> Promise<Bool> {
return Promise.async {
do {
Expand Down
1 change: 1 addition & 0 deletions libraries/react-native-iap/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
26 changes: 26 additions & 0 deletions libraries/react-native-iap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 8 additions & 0 deletions libraries/react-native-iap/src/specs/RnIap.nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,14 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> {
*/
getPendingTransactionsIOS(): Promise<NitroPurchase[]>;

/**
* Get the full StoreKit 2 transaction history as PurchaseIOS values.
* Requires SK2ConsumableTransactionHistory Info.plist key for finished consumables (iOS 18+).
* @returns Promise<NitroPurchase[]> - Array of all transactions
* @platform iOS
*/
getAllTransactionsIOS(): Promise<NitroPurchase[]>;

/**
* Sync with the App Store (iOS only)
* @returns Promise<boolean> - Success flag
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/pages/docs/foundation/founding-supporters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ function FoundingSupporters() {
keywords="OpenIAP founding supporter, open source sponsor, IAP standard supporter"
/>
<h1>Become a Founding Supporter</h1>
<div
style={{
background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderLeft: '3px solid var(--primary-color)',
borderRadius: '4px',
padding: '0.75rem 1rem',
marginBottom: '1.5rem',
fontSize: '0.9rem',
color: 'var(--text-secondary)',
}}
>
<strong>Draft</strong> — The Foundation section is currently being
prepared. Content may change as the governance structure is finalized.
</div>
<p>
We're building OpenIAP into a vendor-neutral, open standard for in-app
purchases — and we're looking for organizations to join as Founding
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/pages/docs/foundation/governance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ function Governance() {
keywords="OpenIAP governance, open source governance, TSC, technical steering committee, maintainer policy"
/>
<h1>Project Governance</h1>
<div
style={{
background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderLeft: '3px solid var(--primary-color)',
borderRadius: '4px',
padding: '0.75rem 1rem',
marginBottom: '1.5rem',
fontSize: '0.9rem',
color: 'var(--text-secondary)',
}}
>
<strong>Draft</strong> — The Foundation section is currently being
prepared. Content may change as the governance structure is finalized.
</div>
<p>
OpenIAP is an open-source project providing a neutral interoperability
standard for in-app purchase APIs and verification across platforms.
Expand Down
Loading
Loading