import Foundation import CoreLocation import SwiftSignalKit import StoreKit import TelegramCore import Postbox import TelegramStringFormatting import TelegramUIPreferences import PersistentStringHash private let productIdentifiers = [ "org.telegram.telegramPremium.annual", "org.telegram.telegramPremium.semiannual", "org.telegram.telegramPremium.monthly", "org.telegram.telegramPremium.twelveMonths", "org.telegram.telegramPremium.sixMonths", "org.telegram.telegramPremium.threeMonths", "org.telegram.telegramPremium.threeMonths.code_x1", "org.telegram.telegramPremium.sixMonths.code_x1", "org.telegram.telegramPremium.twelveMonths.code_x1", "org.telegram.telegramPremium.threeMonths.code_x5", "org.telegram.telegramPremium.sixMonths.code_x5", "org.telegram.telegramPremium.twelveMonths.code_x5", "org.telegram.telegramPremium.threeMonths.code_x10", "org.telegram.telegramPremium.sixMonths.code_x10", "org.telegram.telegramPremium.twelveMonths.code_x10", "org.telegram.telegramPremium.oneWeek.auth", "org.telegram.telegramStars.topup.x15", "org.telegram.telegramStars.topup.x25", "org.telegram.telegramStars.topup.x50", "org.telegram.telegramStars.topup.x75", "org.telegram.telegramStars.topup.x100", "org.telegram.telegramStars.topup.x150", "org.telegram.telegramStars.topup.x250", "org.telegram.telegramStars.topup.x350", "org.telegram.telegramStars.topup.x500", "org.telegram.telegramStars.topup.x750", "org.telegram.telegramStars.topup.x1000", "org.telegram.telegramStars.topup.x1500", "org.telegram.telegramStars.topup.x2500", "org.telegram.telegramStars.topup.x5000", "org.telegram.telegramStars.topup.x10000", "org.telegram.telegramStars.topup.x25000", "org.telegram.telegramStars.topup.x35000" ] 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)) } func prettyPrice() -> NSDecimalNumber { return self.multiplying(by: NSDecimalNumber(value: 2)) .rounding(accordingToBehavior: NSDecimalNumberHandler( roundingMode: .plain, scale: Int16(0), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false ) ) .dividing(by: NSDecimalNumber(value: 2)) .subtracting(NSDecimalNumber(value: 0.01)) } } 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 { return self.skProduct.subscriptionPeriod != nil } } 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 func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String { let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2) let prettierPrice = price .multiplying(by: NSDecimalNumber(value: 2)) .rounding(accordingToBehavior: NSDecimalNumberHandler( roundingMode: .up, scale: Int16(0), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false ) ) .dividing(by: NSDecimalNumber(value: 2)) .subtracting(NSDecimalNumber(value: 0.01)) return self.numberFormatter.string(from: prettierPrice) ?? "" } public func multipliedPrice(count: Int) -> String { let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2) let prettierPrice = price .multiplying(by: NSDecimalNumber(value: 2)) .rounding(accordingToBehavior: NSDecimalNumberHandler( roundingMode: .up, scale: Int16(0), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false ) ) .dividing(by: NSDecimalNumber(value: 2)) .subtracting(NSDecimalNumber(value: 0.01)) return self.numberFormatter.string(from: prettierPrice) ?? "" } 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 case tryLater } public enum RestoreState { case succeed(Bool) case failed } private final class PaymentTransactionContext { var state: SKPaymentTransactionState? let purpose: PendingInAppPurchaseState.Purpose let subscriber: (TransactionState) -> Void init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) { self.purpose = purpose 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: SomeTelegramEngine private var products: [Product] = [] private var productsPromise = Promise<[Product]>([]) private var productRequest: SKProductsRequest? private let stateQueue = Queue() private var paymentContexts: [String: PaymentTransactionContext] = [:] private var finishedSuccessfulTransactions = Set() private var onRestoreCompletion: ((RestoreState) -> Void)? private let disposableSet = DisposableDict() private var lastRequestTimestamp: Double? public init(engine: SomeTelegramEngine) { self.engine = engine super.init() // SKPaymentQueue.default().add(self) // MARK: Swiftgram self.requestProducts() } deinit { // SKPaymentQueue.default().remove(self) // MARK: Swiftgram } var canMakePayments: Bool { return SKPaymentQueue.canMakePayments() } private func requestProducts() { if ({ return true }()) { return } // MARK: Swiftgram Logger.shared.log("InAppPurchaseManager", "Requesting products") let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers)) productRequest.delegate = self productRequest.start() self.productRequest = productRequest self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent() } public var availableProducts: Signal<[Product], NoError> { if self.products.isEmpty { if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 { Logger.shared.log("InAppPurchaseManager", "No available products, rerequest") 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, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal { if !self.canMakePayments { return .fail(.cantMakePayments) } let accountPeerId: String switch self.engine { case let .authorized(engine): accountPeerId = "\(engine.account.peerId.toInt64())" case let .unauthorized(engine): accountPeerId = "\(engine.account.id.int64)" } Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") let purpose = PendingInAppPurchaseState.Purpose(appStorePurpose: purpose) let payment = SKMutablePayment(product: product.skProduct) payment.applicationUsername = accountPeerId payment.quantity = Int(quantity) // SKPaymentQueue.default().add(payment) // MARK: Swiftgram let productIdentifier = payment.productIdentifier let signal = Signal { subscriber in let disposable = MetaDisposable() self.stateQueue.async { let paymentContext = PaymentTransactionContext(purpose: purpose, 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 case .unknown: if let _ = error.userInfo["tryLater"] { mappedError = .tryLater } else { mappedError = .generic } 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 } public struct ReceiptPurchase: Equatable { public let productId: String public let transactionId: String public let expirationDate: Date } public func getReceiptPurchases() -> [ReceiptPurchase] { guard let data = getReceiptData(), let receipt = parseReceipt(data) else { return [] } return receipt.purchases.map { ReceiptPurchase(productId: $0.productId, transactionId: $0.transactionId, expirationDate: $0.expirationDate) } } } 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: String switch self.engine { case let .authorized(engine): accountPeerId = "\(engine.account.peerId.toInt64())" case let .unauthorized(engine): accountPeerId = "\(engine.account.id.int64)" } 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: if transaction.payment.productIdentifier.contains(".topup."), let transactionIdentifier = transaction.transactionIdentifier, self.finishedSuccessfulTransactions.contains(transactionIdentifier) { Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") seems to be already reported, ask to try later") transactionState = .failed(error: SKError(SKError.Code.unknown, userInfo: ["tryLater": true])) queue.finishTransaction(transaction) } else { 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 if let paymentContext = self.paymentContexts[transaction.payment.productIdentifier] { let _ = updatePendingInAppPurchaseState( engine: self.engine, productId: transaction.payment.productIdentifier, content: PendingInAppPurchaseState( productId: transaction.payment.productIdentifier, purpose: paymentContext.purpose ) ).start() } 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)]") guard let transaction = transactionsToAssign.first else { return } let productIdentifier = transaction.payment.productIdentifier var completion: Signal = .never() let products = self.availableProducts |> filter { products in return !products.isEmpty } |> take(1) let product: Signal = products |> map { products in if let product = products.first(where: { $0.id == productIdentifier }) { return product } else { return nil } } let purpose: Signal if let paymentContext = paymentContexts[productIdentifier] { purpose = product |> map { product in return paymentContext.purpose.appStorePurpose(product: product) } } else { purpose = combineLatest( product, pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) ) |> mapToSignal { product, state -> Signal in if let state { return .single(state.purpose.appStorePurpose(product: product)) } else { return .complete() } } } completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil) let receiptData = getReceiptData() ?? Data() #if DEBUG self.debugSaveReceipt(receiptData: receiptData) #endif for transaction in transactionsToAssign { if let transactionIdentifier = transaction.transactionIdentifier { self.finishedSuccessfulTransactions.insert(transactionIdentifier) } } self.disposableSet.set( (purpose |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in switch self.engine { case let .authorized(engine): return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) case let .unauthorized(engine): return 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) } let _ = completion.start() }), 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() { let signal: Signal switch self.engine { case let .authorized(engine): signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) case let .unauthorized(engine): signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) } self.disposableSet.set( signal.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 } } } private func debugSaveReceipt(receiptData: Data) { guard case let .authorized(engine) = self.engine else { return } let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start() } } private final class PendingInAppPurchaseState: Codable { enum CodingKeys: String, CodingKey { case productId case purpose case storeProductId } enum Purpose: Codable { enum DecodingError: Error { case generic } enum CodingKeys: String, CodingKey { case type case peer case peers case boostPeer case additionalPeerIds case countries case onlyNewSubscribers case showWinners case prizeDescription case randomId case untilDate case stars case users case text case entities case restore case phoneNumber case phoneCodeHash } enum PurposeType: Int32 { case subscription case upgrade case restore case gift case giftCode case giveaway case stars case starsGift case starsGiveaway case authCode } case subscription case upgrade case restore case gift(peerId: EnginePeer.Id) case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) case stars(count: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32) case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = PurposeType(rawValue: try container.decode(Int32.self, forKey: .type)) switch type { case .subscription: self = .subscription case .upgrade: self = .upgrade case .restore: self = .restore case .gift: self = .gift( peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)) ) case .giftCode: self = .giftCode( peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) }, boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }), text: try container.decodeIfPresent(String.self, forKey: .text), entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities) ) case .giveaway: self = .giveaway( boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)), additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) }, countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [], onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers), showWinners: try container.decodeIfPresent(Bool.self, forKey: .showWinners) ?? false, prizeDescription: try container.decodeIfPresent(String.self, forKey: .prizeDescription), randomId: try container.decode(Int64.self, forKey: .randomId), untilDate: try container.decode(Int32.self, forKey: .untilDate) ) case .stars: self = .stars( count: try container.decode(Int64.self, forKey: .stars) ) case .starsGift: self = .starsGift( peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)), count: try container.decode(Int64.self, forKey: .stars) ) case .starsGiveaway: self = .starsGiveaway( stars: try container.decode(Int64.self, forKey: .stars), boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)), additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) }, countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [], onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers), showWinners: try container.decodeIfPresent(Bool.self, forKey: .showWinners) ?? false, prizeDescription: try container.decodeIfPresent(String.self, forKey: .prizeDescription), randomId: try container.decode(Int64.self, forKey: .randomId), untilDate: try container.decode(Int32.self, forKey: .untilDate), users: try container.decode(Int32.self, forKey: .users) ) case .authCode: self = .authCode( restore: try container.decode(Bool.self, forKey: .restore), phoneNumber: try container.decode(String.self, forKey: .phoneNumber), phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash) ) default: throw DecodingError.generic } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .subscription: try container.encode(PurposeType.subscription.rawValue, forKey: .type) case .upgrade: try container.encode(PurposeType.upgrade.rawValue, forKey: .type) case .restore: try container.encode(PurposeType.restore.rawValue, forKey: .type) case let .gift(peerId): try container.encode(PurposeType.gift.rawValue, forKey: .type) try container.encode(peerId.toInt64(), forKey: .peer) case let .giftCode(peerIds, boostPeer, text, entities): try container.encode(PurposeType.giftCode.rawValue, forKey: .type) try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers) try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer) try container.encodeIfPresent(text, forKey: .text) try container.encodeIfPresent(entities, forKey: .entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): try container.encode(PurposeType.giveaway.rawValue, forKey: .type) try container.encode(boostPeer.toInt64(), forKey: .boostPeer) try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds) try container.encode(countries, forKey: .countries) try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers) try container.encode(showWinners, forKey: .showWinners) try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription) try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) case let .stars(count): try container.encode(PurposeType.stars.rawValue, forKey: .type) try container.encode(count, forKey: .stars) case let .starsGift(peerId, count): try container.encode(PurposeType.starsGift.rawValue, forKey: .type) try container.encode(peerId.toInt64(), forKey: .peer) try container.encode(count, forKey: .stars) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): try container.encode(PurposeType.starsGiveaway.rawValue, forKey: .type) try container.encode(stars, forKey: .stars) try container.encode(boostPeer.toInt64(), forKey: .boostPeer) try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds) try container.encode(countries, forKey: .countries) try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers) try container.encode(showWinners, forKey: .showWinners) try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription) try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) try container.encode(users, forKey: .users) case let .authCode(restore, phoneNumber, phoneCodeHash): try container.encode(PurposeType.authCode.rawValue, forKey: .type) try container.encode(restore, forKey: .restore) try container.encode(phoneNumber, forKey: .phoneNumber) try container.encode(phoneCodeHash, forKey: .phoneCodeHash) } } init(appStorePurpose: AppStoreTransactionPurpose) { switch appStorePurpose { case .subscription: self = .subscription case .upgrade: self = .upgrade case .restore: self = .restore case let .gift(peerId, _, _): self = .gift(peerId: peerId) case let .giftCode(peerIds, boostPeer, _, _, text, entities): self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _): self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) case let .stars(count, _, _): self = .stars(count: count) case let .starsGift(peerId, count, _, _): self = .starsGift(peerId: peerId, count: count) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users): self = .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, users: users) case let .authCode(restore, phoneNumber, phoneCodeHash, _, _): self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) } } func appStorePurpose(product: InAppPurchaseManager.Product?) -> AppStoreTransactionPurpose { let (currency, amount) = product?.priceCurrencyAndAmount ?? ("", 0) switch self { case .subscription: return .subscription case .upgrade: return .upgrade case .restore: return .restore case let .gift(peerId): return .gift(peerId: peerId, currency: currency, amount: amount) case let .giftCode(peerIds, boostPeer, text, entities): return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) case let .stars(count): return .stars(count: count, currency: currency, amount: amount) case let .starsGift(peerId, count): return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): return .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users) case let .authCode(restore, phoneNumber, phoneCodeHash): return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount) } } } public let productId: String public let purpose: Purpose public init(productId: String, purpose: Purpose) { self.productId = productId self.purpose = purpose } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.productId = try container.decode(String.self, forKey: .productId) self.purpose = try container.decode(Purpose.self, forKey: .purpose) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.productId, forKey: .productId) try container.encode(self.purpose, forKey: .purpose) } } private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) switch engine { case let .authorized(engine): return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)) |> map { entry -> PendingInAppPurchaseState? in return entry?.get(PendingInAppPurchaseState.self) } case let .unauthorized(engine): return engine.itemCache.get(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) |> map { entry -> PendingInAppPurchaseState? in return entry?.get(PendingInAppPurchaseState.self) } } } private func updatePendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) switch engine { case let .authorized(engine): if let content = content { return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) } else { return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) } case let .unauthorized(engine): if let content = content { return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) } else { return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) } } }