mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
928 lines
44 KiB
Swift
928 lines
44 KiB
Swift
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<String>()
|
|
|
|
private var onRestoreCompletion: ((RestoreState) -> Void)?
|
|
|
|
private let disposableSet = DisposableDict<String>()
|
|
|
|
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<PurchaseState, PurchaseError> {
|
|
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<PurchaseState, PurchaseError> { 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, NoError> = .never()
|
|
|
|
let products = self.availableProducts
|
|
|> filter { products in
|
|
return !products.isEmpty
|
|
}
|
|
|> take(1)
|
|
|
|
let product: Signal<InAppPurchaseManager.Product?, NoError> = products
|
|
|> map { products in
|
|
if let product = products.first(where: { $0.id == productIdentifier }) {
|
|
return product
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
let purpose: Signal<AppStoreTransactionPurpose, NoError>
|
|
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<AppStoreTransactionPurpose, NoError> 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<Never, AssignAppStoreTransactionError> 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<Never, AssignAppStoreTransactionError>
|
|
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<PendingInAppPurchaseState?, NoError> {
|
|
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<Never, NoError> {
|
|
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)
|
|
}
|
|
}
|
|
}
|