Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InAppPurchaseManager",
module_name = "InAppPurchaseManager",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/PersistentStringHash:PersistentStringHash",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
@@ -0,0 +1,928 @@
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)
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
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)
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, peerId: EnginePeer.Id?)
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),
peerId: try container.decodeIfPresent(Int64.self, forKey: .peer).flatMap { EnginePeer.Id($0) }
)
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, peerId):
try container.encode(PurposeType.stars.rawValue, forKey: .type)
try container.encode(count, forKey: .stars)
try container.encodeIfPresent(peerId?.toInt64(), forKey: .peer)
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, _, _, peerId):
self = .stars(count: count, peerId: peerId)
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, peerId):
return .stars(count: count, currency: currency, amount: amount, peerId: peerId)
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)
}
}
}
@@ -0,0 +1,259 @@
import Foundation
private struct Asn1Tag {
static let integer: Int32 = 0x02
static let octetString: Int32 = 0x04
static let objectIdentifier: Int32 = 0x06
static let sequence: Int32 = 0x10
static let set: Int32 = 0x11
static let utf8String: Int32 = 0x0c
static let date: Int32 = 0x16
}
private struct Asn1Entry {
let tag: Int32
let data: Data
let length: Int
}
private func parse(_ data: Data, startIndex: Int = 0) -> Asn1Entry {
var index = startIndex
var value = data[index]
index += 1
var tagValue = Int32(value & 0x1f)
if tagValue == 31 {
value = data[index]
index += 1
while (value & 0x80) != 0 {
tagValue <<= 8
tagValue |= Int32(value & 0x7f)
value = data[index]
index += 1
}
tagValue <<= 8
tagValue |= Int32(value & 0x7f)
}
var length = 0
var nextTag = 0
value = data[index]
index += 1
if value & 0x80 == 0 {
length = Int(value)
nextTag = index + length
} else if value != 0x80 {
let octetsCount = Int(value & 0x7f)
for _ in 0 ..< octetsCount {
length <<= 8
value = data[index]
index += 1
length |= Int(value) & 0xff
}
nextTag = index + length
} else {
var scanIndex = index
while data[scanIndex] != 0 && data[scanIndex + 1] != 0 {
scanIndex += 1
}
length = scanIndex - index
nextTag = scanIndex + 2
}
return Asn1Entry(tag: tagValue, data: data.subdata(in: index ..< (index + length)), length: nextTag - startIndex)
}
private func parseSequence(_ data: Data) -> [Asn1Entry] {
var result : [Asn1Entry] = []
var index = 0
while index < data.count {
let entry = parse(data, startIndex: index)
result.append(entry)
index += entry.length
}
return result
}
private func parseInteger(_ data: Data) -> Int32 {
let length = data.count
var value: Int32 = 0
for i in 0 ..< length {
if i == 0 {
value = Int32(data[i] & 0x7f)
} else {
value <<= 8
value |= Int32(data[i])
}
}
if length > 0 && data[0] & 0x80 != 0 {
let complement: Int32 = 1 << (length * 8)
value -= complement
}
return value
}
private func parseObjectIdentifier(_ data: Data, startIndex: Int = 0, length: Int? = nil) -> [Int32] {
let dataLen = length ?? data.count
var index = startIndex
var identifier: [Int32] = []
while index < startIndex + dataLen {
var subidentifier: Int32 = 0
var value = data[index]
index += 1
while (value & 0x80) != 0 {
subidentifier <<= 7
subidentifier |= Int32(value & 0x7f)
value = data[index]
index += 1
}
subidentifier <<= 7
subidentifier |= Int32(value & 0x7f)
identifier.append(subidentifier)
}
return identifier
}
private struct ObjectIdentifier {
static let pkcs7Data: [Int32] = [42, 840, 113549, 1, 7, 1]
static let pkcs7SignedData: [Int32] = [42, 840, 113549, 1, 7, 2]
}
struct Receipt {
fileprivate struct Tag {
static let purchases: Int32 = 17
}
struct Purchase {
fileprivate struct Tag {
static let productIdentifier: Int32 = 1702
static let transactionIdentifier: Int32 = 1703
static let expirationDate: Int32 = 1708
}
let productId: String
let transactionId: String
let expirationDate: Date
}
let purchases: [Purchase]
}
func parseReceipt(_ data: Data) -> Receipt? {
let root = parseSequence(data)
guard root.count == 1 && root[0].tag == Asn1Tag.sequence else {
return nil
}
let rootSeq = parseSequence(root[0].data)
guard rootSeq.count == 2 && rootSeq[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(rootSeq[0].data) == ObjectIdentifier.pkcs7SignedData else {
return nil
}
let signedData = parseSequence(rootSeq[1].data)
guard signedData.count == 1 && signedData[0].tag == Asn1Tag.sequence else {
return nil
}
let signedDataSeq = parseSequence(signedData[0].data)
guard signedDataSeq.count > 3 && signedDataSeq[2].tag == Asn1Tag.sequence else {
return nil
}
let contentData = parseSequence(signedDataSeq[2].data)
guard contentData.count == 2 && contentData[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(contentData[0].data) == ObjectIdentifier.pkcs7Data else {
return nil
}
let payload = parse(contentData[1].data)
guard payload.tag == Asn1Tag.octetString else {
return nil
}
let payloadRoot = parse(payload.data)
guard payloadRoot.tag == Asn1Tag.set else {
return nil
}
var purchases: [Receipt.Purchase] = []
let receiptAttributes = parseSequence(payloadRoot.data)
for attribute in receiptAttributes {
if attribute.tag != Asn1Tag.sequence { continue }
let attributeEntries = parseSequence(attribute.data)
guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil
}
let type = parseInteger(attributeEntries[0].data)
let value = attributeEntries[2].data
switch (type) {
case Receipt.Tag.purchases:
if let purchase = parsePurchaseAttributes(value) {
purchases.append(purchase)
}
default:
break
}
}
return Receipt(purchases: purchases)
}
private func parseRfc3339Date(_ str: String) -> Date? {
let posixLocale = Locale(identifier: "en_US_POSIX")
let formatter1 = DateFormatter()
formatter1.locale = posixLocale
formatter1.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssX5"
formatter1.timeZone = TimeZone(secondsFromGMT: 0)
let result = formatter1.date(from: str)
if result != nil {
return result
}
let formatter2 = DateFormatter()
formatter2.locale = posixLocale
formatter2.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSSSSX5"
formatter2.timeZone = TimeZone(secondsFromGMT: 0)
return formatter2.date(from: str)
}
private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? {
let root = parse(data)
guard root.tag == Asn1Tag.set else {
return nil
}
var productId: String?
var transactionId: String?
var expirationDate: Date?
let receiptAttributes = parseSequence(root.data)
for attribute in receiptAttributes {
if attribute.tag != Asn1Tag.sequence { continue }
let attributeEntries = parseSequence(attribute.data)
guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil
}
let type = parseInteger(attributeEntries[0].data)
let value = attributeEntries[2].data
switch (type) {
case Receipt.Purchase.Tag.productIdentifier:
let valEntry = parse(value)
guard valEntry.tag == Asn1Tag.utf8String else { return nil }
productId = String(bytes: valEntry.data, encoding: .utf8)
case Receipt.Purchase.Tag.transactionIdentifier:
let valEntry = parse(value)
guard valEntry.tag == Asn1Tag.utf8String else { return nil }
transactionId = String(bytes: valEntry.data, encoding: .utf8)
case Receipt.Purchase.Tag.expirationDate:
let valEntry = parse(value)
guard valEntry.tag == Asn1Tag.date else { return nil }
expirationDate = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
default:
break
}
}
guard let productId, let transactionId, let expirationDate else {
return nil
}
return Receipt.Purchase(productId: productId, transactionId: transactionId, expirationDate: expirationDate)
}