Files
amnezia-client/client/platforms/ios/StoreKit2Helper.swift
T

121 lines
5.6 KiB
Swift
Raw Normal View History

2026-03-24 15:56:01 +08:00
import Foundation
import StoreKit
@available(iOS 15.0, macOS 12.0, *)
@objcMembers
public class StoreKit2Helper: NSObject {
public static let shared = StoreKit2Helper()
private struct EntitlementInfo {
let transactionId: UInt64
let originalTransactionId: UInt64
let productId: String
let purchaseDate: Date
var dictionary: NSDictionary {
[
"transactionId": String(transactionId),
"originalTransactionId": String(originalTransactionId),
"productId": productId
]
}
}
2026-03-24 15:56:01 +08:00
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
Task { @MainActor in
2026-03-24 15:56:01 +08:00
do {
try await AppStore.sync()
var entitlements: [EntitlementInfo] = []
2026-03-24 15:56:01 +08:00
for await result in Transaction.currentEntitlements {
switch result {
case .verified(let transaction):
entitlements.append(EntitlementInfo(transactionId: transaction.id,
originalTransactionId: transaction.originalID,
productId: transaction.productID,
purchaseDate: transaction.purchaseDate))
2026-03-24 15:56:01 +08:00
case .unverified(_, let error):
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
}
}
let sortedEntitlements = entitlements.sorted { lhs, rhs in
if lhs.purchaseDate != rhs.purchaseDate {
return lhs.purchaseDate > rhs.purchaseDate
}
return lhs.transactionId > rhs.transactionId
}.map { $0.dictionary }
completion(true, sortedEntitlements, nil)
} catch {
completion(false, nil, error as NSError)
2026-03-24 15:56:01 +08:00
}
}
}
public func purchaseProduct(productIdentifier: String, completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void) {
Task {
do {
let products = try await Product.products(for: [productIdentifier])
guard let product = products.first else {
let error = NSError(domain: "StoreKit2Helper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Product not found"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
return
}
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
let txId = String(transaction.id)
let origTxId = String(transaction.originalID)
let pId = transaction.productID
DispatchQueue.main.async { completion(true, txId, pId, origTxId, nil) }
case .unverified(_, let error):
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
}
case .userCancelled:
let error = NSError(domain: "StoreKit2Helper", code: 1, userInfo: [NSLocalizedDescriptionKey: "Purchase cancelled"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
case .pending:
let error = NSError(domain: "StoreKit2Helper", code: 2, userInfo: [NSLocalizedDescriptionKey: "Purchase pending"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
@unknown default:
let error = NSError(domain: "StoreKit2Helper", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
}
} catch {
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
}
}
}
private func storefrontCurrencyCode(for product: Product) -> String {
product.priceFormatStyle.locale.currencyCode ?? ""
}
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
Task {
do {
let products = try await Product.products(for: identifiers)
let productDicts = products.map { product -> NSDictionary in
let currencyCode = storefrontCurrencyCode(for: product)
return [
"productId": product.id,
"title": product.displayName,
"description": product.description,
"price": "\(product.price)",
"displayPrice": product.displayPrice,
2026-03-24 15:56:01 +08:00
"currencyCode": currencyCode
]
}
let fetchedIds = Set(products.map { $0.id })
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
} catch {
DispatchQueue.main.async { completion([], Array(identifiers), error as NSError) }
}
}
}
}