import Foundation import CoreLocation import SwiftSignalKit import StoreKit import Postbox import TelegramCore import TelegramStringFormatting private let productIdentifiers = [ "org.telegram.telegramPremium.monthly", "org.telegram.telegramPremium.twelveMonths", "org.telegram.telegramPremium.sixMonths", "org.telegram.telegramPremium.threeMonths" ] private extension NSDecimalNumber { func round(_ decimals: Int) -> NSDecimalNumber { return self.rounding(accordingToBehavior: NSDecimalNumberHandler(roundingMode: .down, scale: Int16(decimals), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)) } } public final class InAppPurchaseManager: NSObject { public final class Product: Equatable { private lazy var numberFormatter: NumberFormatter = { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .currency numberFormatter.locale = self.skProduct.priceLocale return numberFormatter }() let skProduct: SKProduct init(skProduct: SKProduct) { self.skProduct = skProduct } public var id: String { return self.skProduct.productIdentifier } public var isSubscription: Bool { if #available(iOS 12.0, *) { return self.skProduct.subscriptionGroupIdentifier != nil } else if #available(iOS 11.2, *) { return self.skProduct.subscriptionPeriod != nil } else { return self.id.contains(".monthly") } } public var price: String { return self.numberFormatter.string(from: self.skProduct.price) ?? "" } public func pricePerMonth(_ monthsCount: Int) -> String { let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2) return self.numberFormatter.string(from: price) ?? "" } public var priceValue: NSDecimalNumber { return self.skProduct.price } public var priceCurrencyAndAmount: (currency: String, amount: Int64) { if let currencyCode = self.numberFormatter.currencyCode, let amount = fractionalToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) { return (currencyCode, amount) } else { return ("", 0) } } public static func ==(lhs: Product, rhs: Product) -> Bool { if lhs.id != rhs.id { return false } if lhs.isSubscription != rhs.isSubscription { return false } if lhs.priceValue != rhs.priceValue { return false } return true } } public enum PurchaseState { case purchased(transactionId: String) } public enum PurchaseError { case generic case cancelled case network case notAllowed case cantMakePayments case assignFailed } public enum RestoreState { case succeed(Bool) case failed } private final class PaymentTransactionContext { var state: SKPaymentTransactionState? var targetPeerId: PeerId? let subscriber: (TransactionState) -> Void init(targetPeerId: PeerId?, subscriber: @escaping (TransactionState) -> Void) { self.targetPeerId = targetPeerId self.subscriber = subscriber } } private enum TransactionState { case purchased(transactionId: String?) case restored(transactionId: String?) case purchasing case failed(error: SKError?) case assignFailed case deferred } private let engine: TelegramEngine private var products: [Product] = [] private var productsPromise = Promise<[Product]>([]) private var productRequest: SKProductsRequest? private let stateQueue = Queue() private var paymentContexts: [String: PaymentTransactionContext] = [:] private var onRestoreCompletion: ((RestoreState) -> Void)? private let disposableSet = DisposableDict() public init(engine: TelegramEngine) { self.engine = engine super.init() SKPaymentQueue.default().add(self) self.requestProducts() } deinit { SKPaymentQueue.default().remove(self) } var canMakePayments: Bool { return SKPaymentQueue.canMakePayments() } private func requestProducts() { Logger.shared.log("InAppPurchaseManager", "Requesting products") let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers)) productRequest.delegate = self productRequest.start() self.productRequest = productRequest } public var availableProducts: Signal<[Product], NoError> { if self.products.isEmpty && self.productRequest == nil { self.requestProducts() } return self.productsPromise.get() } public func restorePurchases(completion: @escaping (RestoreState) -> Void) { Logger.shared.log("InAppPurchaseManager", "Restoring purchases") self.onRestoreCompletion = completion let paymentQueue = SKPaymentQueue.default() paymentQueue.restoreCompletedTransactions() } public func finishAllTransactions() { Logger.shared.log("InAppPurchaseManager", "Finishing all transactions") let paymentQueue = SKPaymentQueue.default() let transactions = paymentQueue.transactions for transaction in transactions { paymentQueue.finishTransaction(transaction) } } public func buyProduct(_ product: Product, targetPeerId: PeerId? = nil) -> Signal { if !self.canMakePayments { return .fail(.cantMakePayments) } if !product.isSubscription && targetPeerId == nil { return .fail(.cantMakePayments) } let accountPeerId = "\(self.engine.account.peerId.toInt64())" Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") let payment = SKMutablePayment(product: product.skProduct) payment.applicationUsername = accountPeerId SKPaymentQueue.default().add(payment) let productIdentifier = payment.productIdentifier let signal = Signal { subscriber in let disposable = MetaDisposable() self.stateQueue.async { let paymentContext = PaymentTransactionContext(targetPeerId: targetPeerId, subscriber: { state in switch state { case let .purchased(transactionId), let .restored(transactionId): if let transactionId = transactionId { subscriber.putNext(.purchased(transactionId: transactionId)) subscriber.putCompletion() } else { subscriber.putError(.generic) } case let .failed(error): if let error = error { let mappedError: PurchaseError switch error.code { case .paymentCancelled: mappedError = .cancelled case .cloudServiceNetworkConnectionFailed, .cloudServicePermissionDenied: mappedError = .network case .paymentNotAllowed, .clientInvalid: mappedError = .notAllowed default: mappedError = .generic } subscriber.putError(mappedError) } else { subscriber.putError(.generic) } case .assignFailed: subscriber.putError(.assignFailed) case .deferred, .purchasing: break } }) self.paymentContexts[productIdentifier] = paymentContext disposable.set(ActionDisposable { [weak paymentContext] in self.stateQueue.async { if let current = self.paymentContexts[productIdentifier], current === paymentContext { self.paymentContexts.removeValue(forKey: productIdentifier) } } }) } return disposable } return signal } } extension InAppPurchaseManager: SKProductsRequestDelegate { public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { self.productRequest = nil Queue.mainQueue().async { let products = response.products.map { Product(skProduct: $0) } Logger.shared.log("InAppPurchaseManager", "Received products \(products.map({ $0.skProduct.productIdentifier }).joined(separator: ", "))") self.productsPromise.set(.single(products)) } } } private func getReceiptData() -> Data? { var receiptData: Data? if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) } catch { Logger.shared.log("InAppPurchaseManager", "Couldn't read receipt data with error: \(error.localizedDescription)") } } return receiptData } extension InAppPurchaseManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { self.stateQueue.async { let accountPeerId = "\(self.engine.account.peerId.toInt64())" let paymentContexts = self.paymentContexts var transactionsToAssign: [SKPaymentTransaction] = [] for transaction in transactions { if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId { continue } let productIdentifier = transaction.payment.productIdentifier let transactionState: TransactionState? switch transaction.transactionState { case .purchased: Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased") transactionState = .purchased(transactionId: transaction.transactionIdentifier) transactionsToAssign.append(transaction) case .restored: Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring") let transactionIdentifier = transaction.transactionIdentifier transactionState = .restored(transactionId: transactionIdentifier) case .failed: Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")") transactionState = .failed(error: transaction.error as? SKError) queue.finishTransaction(transaction) case .purchasing: Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") purchasing") transactionState = .purchasing case .deferred: Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") deferred") transactionState = .deferred default: transactionState = nil } if let transactionState = transactionState { if let context = self.paymentContexts[productIdentifier] { context.subscriber(transactionState) } } } if !transactionsToAssign.isEmpty { let transactionIds = transactionsToAssign.compactMap({ $0.transactionIdentifier }).joined(separator: ", ") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), sending receipt for transactions [\(transactionIds)]") let transaction = transactionsToAssign.first let purposeSignal: Signal if let productIdentifier = transaction?.payment.productIdentifier, let targetPeerId = paymentContexts[productIdentifier]?.targetPeerId { purposeSignal = self.availableProducts |> filter { products in return !products.isEmpty } |> take(1) |> map { products -> AppStoreTransactionPurpose in if let product = products.first(where: { $0.id == productIdentifier }) { let (currency, amount) = product.priceCurrencyAndAmount return .gift(peerId: targetPeerId, currency: currency, amount: amount) } else { return .gift(peerId: targetPeerId, currency: "", amount: 0) } } } else { purposeSignal = .single(.subscription) } let receiptData = getReceiptData() ?? Data() self.disposableSet.set( (purposeSignal |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) }).start(error: { [weak self] _ in Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign") for transaction in transactions { self?.stateQueue.async { if let strongSelf = self, let context = strongSelf.paymentContexts[transaction.payment.productIdentifier] { context.subscriber(.assignFailed) } } queue.finishTransaction(transaction) } }, completed: { Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] successfully assigned") for transaction in transactions { queue.finishTransaction(transaction) } }), forKey: transactionIds ) } } } public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { Queue.mainQueue().async { if let onRestoreCompletion = self.onRestoreCompletion { Logger.shared.log("InAppPurchaseManager", "Transactions restoration finished") self.onRestoreCompletion = nil if let receiptData = getReceiptData() { self.disposableSet.set( self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore).start(error: { error in Queue.mainQueue().async { if case .serverProvided = error { onRestoreCompletion(.succeed(true)) } else { onRestoreCompletion(.succeed(false)) } } }, completed: { Queue.mainQueue().async { onRestoreCompletion(.succeed(false)) } Logger.shared.log("InAppPurchaseManager", "Sent restored receipt") }), forKey: "restore" ) } else { onRestoreCompletion(.succeed(false)) } } } } public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { Queue.mainQueue().async { if let onRestoreCompletion = self.onRestoreCompletion { Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")") onRestoreCompletion(.failed) self.onRestoreCompletion = nil } } } }