2025-12-16 13:49:08 +08:00
|
|
|
//
|
|
|
|
|
// 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<String> = []
|
|
|
|
|
private var configuredLifetimeIds: Set<String> = []
|
|
|
|
|
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)
|
2025-12-16 14:14:49 +08:00
|
|
|
|
2025-12-16 15:47:12 +08:00
|
|
|
if let payload = await self.fetchPayload(for: productId) {
|
2025-12-16 14:14:49 +08:00
|
|
|
self.verifySignedPayload(payload, completion: completion)
|
|
|
|
|
} else {
|
2025-12-16 13:49:08 +08:00
|
|
|
await MainActor.run {
|
2025-12-16 14:14:49 +08:00
|
|
|
completion(false, "Unable to obtain transaction payload.")
|
2025-12-16 13:49:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 14:14:49 +08:00
|
|
|
private func verifySignedPayload(_ payload: String, completion: @escaping (Bool, String?) -> Void) {
|
|
|
|
|
receiptVerifier.verifySignedPayload(payload) { success, message, _ in
|
2025-12-16 13:49:08 +08:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
if success {
|
|
|
|
|
NotificationCenter.default.post(
|
2025-12-16 14:14:49 +08:00
|
|
|
name: NSNotification.Name.KBIAPDidCompletePurchase,
|
2025-12-16 13:49:08 +08:00
|
|
|
object: nil
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
completion(success, message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 15:47:12 +08:00
|
|
|
@MainActor
|
|
|
|
|
private func fetchPayload(for productId: String) async -> String? {
|
|
|
|
|
if let payload = manager.consumeRecentPayload(for: productId) {
|
|
|
|
|
return payload
|
|
|
|
|
}
|
|
|
|
|
return await Self.latestJWSPayload(for: productId, retryCount: 3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func latestJWSPayload(for productId: String, retryCount: Int = 1) async -> String? {
|
|
|
|
|
var attempts = 0
|
|
|
|
|
while attempts < retryCount {
|
|
|
|
|
if let result = await Transaction.latest(for: productId), case .verified = result {
|
|
|
|
|
return result.jwsRepresentation
|
|
|
|
|
}
|
|
|
|
|
attempts += 1
|
|
|
|
|
if attempts < retryCount {
|
|
|
|
|
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3s
|
|
|
|
|
}
|
2025-12-16 13:49:08 +08:00
|
|
|
}
|
2025-12-16 14:14:49 +08:00
|
|
|
return nil
|
2025-12-16 13:49:08 +08:00
|
|
|
}
|
|
|
|
|
}
|