Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.

Commit 9a7a379

Browse files
committed
Add event listener support following OpenIAP naming conventions
- Add purchaseUpdatedListener and purchaseErrorListener types - Implement listener management methods (add/remove) - Update requestPurchase to notify listeners on purchase events - Add TransactionObserver example showing listener usage - Follow OpenIAP's event-based architecture for purchase operations
1 parent 0b3abf3 commit 9a7a379

File tree

2 files changed

+152
-2
lines changed

2 files changed

+152
-2
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import SwiftUI
2+
import IosIAP
3+
4+
@MainActor
5+
@available(iOS 15.0, *)
6+
class TransactionObserver: ObservableObject {
7+
@Published var latestPurchase: IapPurchase?
8+
@Published var errorMessage: String?
9+
@Published var isPending = false
10+
11+
private let iapModule = IapModule.shared
12+
13+
init() {
14+
setupListeners()
15+
}
16+
17+
deinit {
18+
// Clean up listeners
19+
iapModule.removeAllPurchaseUpdatedListeners()
20+
iapModule.removeAllPurchaseErrorListeners()
21+
}
22+
23+
private func setupListeners() {
24+
// Add purchase updated listener
25+
iapModule.addPurchaseUpdatedListener { [weak self] purchase in
26+
Task { @MainActor in
27+
self?.handlePurchaseUpdated(purchase)
28+
}
29+
}
30+
31+
// Add purchase error listener
32+
iapModule.addPurchaseErrorListener { [weak self] error in
33+
Task { @MainActor in
34+
self?.handlePurchaseError(error)
35+
}
36+
}
37+
}
38+
39+
private func handlePurchaseUpdated(_ purchase: IapPurchase) {
40+
print("✅ Purchase successful: \(purchase.productId)")
41+
latestPurchase = purchase
42+
isPending = false
43+
errorMessage = nil
44+
}
45+
46+
private func handlePurchaseError(_ error: IapError) {
47+
print("❌ Purchase failed: \(error)")
48+
errorMessage = error.localizedDescription
49+
isPending = false
50+
}
51+
}
52+
53+
// Example usage in SwiftUI View
54+
struct TransactionObserverExampleView: View {
55+
@StateObject private var observer = TransactionObserver()
56+
57+
var body: some View {
58+
VStack(spacing: 20) {
59+
Text("Transaction Observer Example")
60+
.font(.title)
61+
62+
if observer.isPending {
63+
ProgressView("Transaction pending...")
64+
}
65+
66+
if let purchase = observer.latestPurchase {
67+
VStack(alignment: .leading) {
68+
Text("Latest Purchase:")
69+
.font(.headline)
70+
Text("Product: \(purchase.productId)")
71+
Text("Date: \(Date(timeIntervalSince1970: purchase.purchaseTime / 1000), formatter: dateFormatter)")
72+
}
73+
.padding()
74+
.background(Color.green.opacity(0.1))
75+
.cornerRadius(8)
76+
}
77+
78+
if let error = observer.errorMessage {
79+
Text("Error: \(error)")
80+
.foregroundColor(.red)
81+
.padding()
82+
}
83+
}
84+
.padding()
85+
}
86+
87+
private var dateFormatter: DateFormatter {
88+
let formatter = DateFormatter()
89+
formatter.dateStyle = .medium
90+
formatter.timeStyle = .short
91+
return formatter
92+
}
93+
}

Sources/IapModule.swift

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import StoreKit
88

99
// MARK: - Helper functions for ExpoModulesCore compatibility
1010

11+
// MARK: - Purchase Listeners
12+
13+
@available(iOS 15.0, macOS 12.0, *)
14+
public typealias PurchaseUpdatedListener = (IapPurchase) -> Void
15+
16+
@available(iOS 15.0, macOS 12.0, *)
17+
public typealias PurchaseErrorListener = (IapError) -> Void
18+
1119
// MARK: - Protocol
1220

1321
@available(iOS 15.0, macOS 12.0, *)
@@ -79,6 +87,9 @@ public final class IapModule: NSObject, IapModuleProtocol {
7987
// Product caching
8088
private var productStore: ProductStore?
8189

90+
// Purchase listeners
91+
private var purchaseUpdatedListeners: [PurchaseUpdatedListener] = []
92+
private var purchaseErrorListeners: [PurchaseErrorListener] = []
8293

8394
// State
8495
private var isInitialized = false
@@ -342,6 +353,14 @@ public final class IapModule: NSObject, IapModuleProtocol {
342353
let transaction = try checkVerified(verification)
343354
let transactionData = transactionToIapTransactionData(transaction, jwsRepresentation: verification.jwsRepresentation)
344355

356+
// Convert to IapPurchase for listener
357+
let purchase = await IapPurchase(from: transaction, jwsRepresentation: verification.jwsRepresentation)
358+
359+
// Notify listeners
360+
for listener in purchaseUpdatedListeners {
361+
listener(purchase)
362+
}
363+
345364
// Store transaction if not finishing automatically
346365
if !andDangerouslyFinishTransactionAutomatically {
347366
pendingTransactions[String(transaction.id)] = transaction
@@ -353,13 +372,22 @@ public final class IapModule: NSObject, IapModuleProtocol {
353372
return transactionData
354373

355374
case .userCancelled:
356-
throw IapError.purchaseCancelled
375+
let error = IapError.purchaseCancelled
376+
for listener in purchaseErrorListeners {
377+
listener(error)
378+
}
379+
throw error
357380

358381
case .pending:
382+
// For deferred payments, we don't call error listeners
359383
throw IapError.purchaseDeferred
360384

361385
@unknown default:
362-
throw IapError.unknownError
386+
let error = IapError.unknownError
387+
for listener in purchaseErrorListeners {
388+
listener(error)
389+
}
390+
throw error
363391
}
364392
}
365393

@@ -864,6 +892,35 @@ public final class IapModule: NSObject, IapModuleProtocol {
864892
}
865893

866894

895+
// MARK: - Listener Management
896+
897+
public func addPurchaseUpdatedListener(_ listener: @escaping PurchaseUpdatedListener) {
898+
purchaseUpdatedListeners.append(listener)
899+
}
900+
901+
public func removePurchaseUpdatedListener(_ listener: @escaping PurchaseUpdatedListener) {
902+
// Note: In Swift, comparing closures is not straightforward
903+
// For now, we provide a method to clear all listeners
904+
// In production, you might want to use a UUID-based system
905+
}
906+
907+
public func removeAllPurchaseUpdatedListeners() {
908+
purchaseUpdatedListeners.removeAll()
909+
}
910+
911+
public func addPurchaseErrorListener(_ listener: @escaping PurchaseErrorListener) {
912+
purchaseErrorListeners.append(listener)
913+
}
914+
915+
public func removePurchaseErrorListener(_ listener: @escaping PurchaseErrorListener) {
916+
// Note: In Swift, comparing closures is not straightforward
917+
// For now, we provide a method to clear all listeners
918+
}
919+
920+
public func removeAllPurchaseErrorListeners() {
921+
purchaseErrorListeners.removeAll()
922+
}
923+
867924
// MARK: - Private Methods
868925

869926
private func ensureConnection() throws {

0 commit comments

Comments
 (0)