// // KBStoreKitBridge.swift // keyBoard // // Created to expose StoreKit2Manager to Objective-C callers. // import Foundation import StoreKit @objcMembers final class KBStoreKitBridge: NSObject, StoreKitDelegate { static let shared = KBStoreKitBridge() private let manager = StoreKit2Manager.shared private let receiptVerifier = IAPVerifyTransactionObj() private var configuredProductIds: Set = [] private var configuredLifetimeIds: Set = [] private var hasConfigured = false private override init() { super.init() } // MARK: - Preparation func prepare(withProductIds productIds: [String], completion: ((Bool, String?) -> Void)? = nil) { prepare(withProductIds: productIds, lifetimeIds: [], completion: completion) } func prepare(withProductIds productIds: [String], lifetimeIds: [String], completion: ((Bool, String?) -> Void)? = nil) { Task { let success = await self.configureIfNeeded(productIds: productIds, lifetimeIds: lifetimeIds) await MainActor.run { completion?(success, success ? nil : "Unable to load products.") } } } // MARK: - Purchase func purchase(productId: String, completion: @escaping (Bool, String?) -> Void) { Task { let ready = await self.configureIfNeeded(productIds: [productId], lifetimeIds: []) guard ready else { await MainActor.run { completion(false, "Unable to load product.") } return } do { try await self.manager.purchase(productId: productId) let state = self.manager.currentState switch state { case .purchaseSuccess(let id) where id == productId: let receipt = try await Self.fetchReceiptData() self.verifyReceipt(receipt, completion: completion) case .purchasePending(let id) where id == productId: await MainActor.run { completion(false, "Purchase pending approval.") } case .purchaseCancelled(let id) where id == productId: await MainActor.run { completion(false, "Purchase cancelled.") } case .purchaseFailed(let id, let error) where id == productId: await MainActor.run { completion(false, error.localizedDescription) } case .error(let error): await MainActor.run { completion(false, error.localizedDescription) } default: await MainActor.run { completion(false, "Purchase failed.") } } } catch { await MainActor.run { completion(false, error.localizedDescription) } } } } // MARK: - Private Helpers @MainActor private func configureIfNeeded(productIds: [String], lifetimeIds: [String]) async -> Bool { let ids = productIds.filter { !$0.isEmpty } guard !ids.isEmpty else { return false } let newProducts = Set(ids) let newLifetime = Set(lifetimeIds.filter { !$0.isEmpty }) var needsConfigure = !hasConfigured if !newProducts.isSubset(of: configuredProductIds) { configuredProductIds.formUnion(newProducts) needsConfigure = true } if !newLifetime.isSubset(of: configuredLifetimeIds) { configuredLifetimeIds.formUnion(newLifetime) needsConfigure = true } if needsConfigure { let config = StoreKitConfig( productIds: Array(configuredProductIds), lifetimeIds: Array(configuredLifetimeIds) ) manager.configure(with: config, delegate: self) hasConfigured = true } await manager.refreshProducts() return true } private func verifyReceipt(_ receipt: String, completion: @escaping (Bool, String?) -> Void) { receiptVerifier.verifyReceipt(receipt) { success, message, _ in DispatchQueue.main.async { if success { NotificationCenter.default.post( name: NSNotification.Name(rawValue: NSNotification.Name.KBIAPDidCompletePurchase.rawValue), object: nil ) } completion(success, message) } } } private static func fetchReceiptData() async throws -> String { if let receipt = try loadReceiptFromBundle() { return receipt } try await AppStore.sync() if let receipt = try loadReceiptFromBundle() { return receipt } throw StoreKitError.verificationFailed } private static func loadReceiptFromBundle() throws -> String? { guard let url = Bundle.main.appStoreReceiptURL else { return nil } guard FileManager.default.fileExists(atPath: url.path) else { return nil } let data = try Data(contentsOf: url, options: .alwaysMapped) guard !data.isEmpty else { return nil } return data.base64EncodedString() } }