diff --git a/Swiftgram/SGAPI/Sources/SGAPI.swift b/Swiftgram/SGAPI/Sources/SGAPI.swift index 5511493040..9a85f8093c 100644 --- a/Swiftgram/SGAPI/Sources/SGAPI.swift +++ b/Swiftgram/SGAPI/Sources/SGAPI.swift @@ -143,3 +143,46 @@ public func getSGAPIRegDate(token: String, deviceToken: String, userId: Int64) - } } } + + +public func postSGReceipt(token: String, deviceToken: String, encodedReceiptData: Data) -> Signal { + return Signal { subscriber in + + let url = URL(string: buildApiUrl("validate"))! + let headers = [ + SG_API_AUTHORIZATION_HEADER: "Token \(token)", + SG_API_DEVICE_TOKEN_HEADER: deviceToken + ] + let completed = Atomic(value: false) + + var request = URLRequest(url: url) + headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + request.httpMethod = "POST" + request.httpBody = encodedReceiptData + + let dataSignal = requestsCustom(request: request).start(next: { data, urlResponse in + let _ = completed.swap(true) + + if let httpResponse = urlResponse as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200...299: + subscriber.putCompletion() + default: + subscriber.putError(.generic("Error posting Receipt: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "")")) + } + } else { + subscriber.putError(.generic("Not an HTTP response: \(String(describing: urlResponse))")) + } + }, error: { error in + subscriber.putError(.generic("Error posting Receipt: \(String(describing: error))")) + }) + + return ActionDisposable { + if !completed.with({ $0 }) { + dataSignal.dispose() + } + } + } +} diff --git a/Swiftgram/SGConfig/Sources/File.swift b/Swiftgram/SGConfig/Sources/File.swift index 3a2a426568..ec1d5f8684 100644 --- a/Swiftgram/SGConfig/Sources/File.swift +++ b/Swiftgram/SGConfig/Sources/File.swift @@ -5,6 +5,7 @@ public struct SGConfig: Codable { public var apiUrl: String = "https://api.swiftgram.app" public var webappUrl: String = "https://my.swiftgram.app" public var botUsername: String = "SwiftgramBot" + public var iaps: [String] = [] } private func parseSGConfig(_ jsonString: String) -> SGConfig { diff --git a/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift index 6c788db7d3..82d0a59302 100644 --- a/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift +++ b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift @@ -839,6 +839,7 @@ private enum SGDebugActions: String { case flexing case fileManager case clearRegDateCache + case debugIAP } private enum SGDebugToggles: String { @@ -862,6 +863,7 @@ private func SGDebugControllerEntries(presentationData: PresentationData) -> [SG #if DEBUG entries.append(.action(id: id.count, section: .base, actionType: .flexing, text: "FLEX", kind: .generic)) entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", kind: .generic)) + entries.append(.action(id: id.count, section: .base, actionType: .debugIAP, text: "Buy", kind: .generic)) #endif if SGSimpleSettings.shared.b { @@ -1020,6 +1022,10 @@ public func sgDebugController(context: AccountContext) -> ViewController { nil) } #endif + case .debugIAP: + #if DEBUG + preconditionFailure("IAP") + #endif } }) diff --git a/Swiftgram/SGIAP/BUILD b/Swiftgram/SGIAP/BUILD new file mode 100644 index 0000000000..bf7f27b0be --- /dev/null +++ b/Swiftgram/SGIAP/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGIAP", + module_name = "SGIAP", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGConfig:SGConfig", + "//submodules/AppBundle:AppBundle" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGIAP/Sources/SGIAP.swift b/Swiftgram/SGIAP/Sources/SGIAP.swift new file mode 100644 index 0000000000..777802d3fc --- /dev/null +++ b/Swiftgram/SGIAP/Sources/SGIAP.swift @@ -0,0 +1,328 @@ +import StoreKit +import SGConfig +import SGLogging +import AppBundle + +private final class CurrencyFormatterEntry { + public let symbol: String + public let thousandsSeparator: String + public let decimalSeparator: String + public let symbolOnLeft: Bool + public let spaceBetweenAmountAndSymbol: Bool + public let decimalDigits: Int + + public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + self.symbol = symbol + self.thousandsSeparator = thousandsSeparator + self.decimalSeparator = decimalSeparator + self.symbolOnLeft = symbolOnLeft + self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol + self.decimalDigits = decimalDigits + } +} + +private func getCurrencyExp(currency: String) -> Int { + switch currency { + case "CLF": + return 4 + case "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND": + return 3 + case "BIF", "BYR", "CLP", "CVE", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "UYI", "VND", "VUV", "XAF", "XOF", "XPF": + return 0 + case "MRO": + return 1 + default: + return 2 + } +} + +private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] { + guard let filePath = getAppBundle().path(forResource: "currencies", ofType: "json") else { + return [:] + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [:] + } + + guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else { + return [:] + } + + var result: [String: CurrencyFormatterEntry] = [:] + + for (code, contents) in dict { + if let contentsDict = contents as? [String: AnyObject] { + let entry = CurrencyFormatterEntry( + symbol: contentsDict["symbol"] as! String, + thousandsSeparator: contentsDict["thousandsSeparator"] as! String, + decimalSeparator: contentsDict["decimalSeparator"] as! String, + symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue, + spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue, + decimalDigits: getCurrencyExp(currency: code.uppercased()) + ) + result[code] = entry + result[code.lowercased()] = entry + } + } + + return result +} + +private let currencyFormatterEntries = loadCurrencyFormatterEntries() + +private func fractionalValueToCurrencyAmount(value: Double, currency: String) -> Int64? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + if value > Double(Int64.max) / factor { + return nil + } else { + return Int64(value * factor) + } +} + + +public extension Notification.Name { + static let SGIAPHelperPurchaseNotification = Notification.Name("SGIAPPurchaseNotification") + static let SGIAPHelperErrorNotification = Notification.Name("SGIAPErrorNotification") +} + +public final class SGIAP: NSObject { + private var productRequest: SKProductsRequest? + private var productsRequestCompletion: (([SKProduct]) -> Void)? + private var purchaseCompletion: ((Bool, Error?) -> Void)? + + + public final class SGProduct: 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 = fractionalValueToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) { + return (currencyCode, amount) + } else { + return ("", 0) + } + } + + public static func ==(lhs: SGProduct, rhs: SGProduct) -> 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 init(foo: Bool = false) { + super.init() + + SKPaymentQueue.default().add(self) + self.requestProducts() + } + + deinit { + SKPaymentQueue.default().remove(self) + } + + var canMakePayments: Bool { + return SKPaymentQueue.canMakePayments() + } + + public func buyProduct(_ product: SKProduct) { + SGLogger.shared.log("SGIAP", "Buying \(product.productIdentifier)...") + let payment = SKPayment(product: product) + SKPaymentQueue.default().add(payment) + } + + private func requestProducts() { + SGLogger.shared.log("SGIAP", "Requesting products...") + let productRequest = SKProductsRequest(productIdentifiers: Set(SG_CONFIG.iaps)) + + productRequest.delegate = self + productRequest.start() + + self.productRequest = productRequest + } + + public func restorePurchases(completion: @escaping (RestoreState) -> Void) { + SGLogger.shared.log("SGIAP", "Restoring purchases...") + + let paymentQueue = SKPaymentQueue.default() + paymentQueue.restoreCompletedTransactions() + } + + public enum RestoreState { + case succeed(Bool) + case failed + } +} + +extension SGIAP: SKProductsRequestDelegate { + public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + self.productRequest = nil + + DispatchQueue.main.async { + let products = response.products + SGLogger.shared.log("SGIAP", "Received products \(products.map({ $0.productIdentifier }).joined(separator: ", "))") + } + } + + public func request(_ request: SKRequest, didFailWithError error: Error) { + SGLogger.shared.log("SGIAP", "Failed to load list of products. Error \(error.localizedDescription)") + self.productRequest = nil + } +} + +extension SGIAP: SKPaymentTransactionObserver { + public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + SGLogger.shared.log("SGIAP", "paymentQueue transactions \(transactions.count)") + for transaction in transactions { + SGLogger.shared.log("SGIAP", "Transaction state for \(transaction.payment.productIdentifier): \(transaction.transactionState)") + switch transaction.transactionState { + case .purchased, .restored: + NotificationCenter.default.post(name: .SGIAPHelperPurchaseNotification, object: transaction) + case .purchasing, .deferred: + break + case .failed: + if let transactionError = transaction.error as NSError?, + let localizedDescription = transaction.error?.localizedDescription, + transactionError.code != SKError.paymentCancelled.rawValue { + SGLogger.shared.log("SGIAP", "Transaction Error: \(localizedDescription)") + } + NotificationCenter.default.post(name: .SGIAPHelperErrorNotification, object: transaction) + default: + SGLogger.shared.log("SGIAP", "Unknown transaction state \(transaction.transactionState). Finishing transaction.") + SKPaymentQueue.default().finishTransaction(transaction) + } + } + } + +} + +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 func getPurchaceReceiptData() -> 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 { + SGLogger.shared.log("SGIAP", "Couldn't read receipt data with error: \(error.localizedDescription)") + } + } else { + SGLogger.shared.log("SGIAP", "Couldn't find receipt path") + } + return receiptData +} diff --git a/Swiftgram/SGIQTP/Sources/SGIQTP.swift b/Swiftgram/SGIQTP/Sources/SGIQTP.swift index ea0f937df7..794d92897c 100644 --- a/Swiftgram/SGIQTP/Sources/SGIQTP.swift +++ b/Swiftgram/SGIQTP/Sources/SGIQTP.swift @@ -23,22 +23,23 @@ public func makeIqtpQuery(_ api: Int, _ method: String, _ args: [String] = []) - } public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { + let queryId = arc4random() #if DEBUG - SGLogger.shared.log("SGIQTP", "Query: \(query)") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Query: \(query)") #else - SGLogger.shared.log("SGIQTP", "Query") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Query") #endif - return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil) + return engine.peers.resolvePeerByName(forIQTP: true, name: SG_CONFIG.botUsername, referrer: nil) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { - SGLogger.shared.log("SGIQTP", "Failed to resolve peer") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to resolve peer \(SG_CONFIG.botUsername)") return .complete() } return .single(result) } |> mapToSignal { peer -> Signal in guard let peer = peer else { - SGLogger.shared.log("SGIQTP", "Empty peer") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty peer") return .single(nil) } return engine.messages.requestChatContextResults(botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) @@ -46,13 +47,13 @@ public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults return results?.results } |> `catch` { error -> Signal in - SGLogger.shared.log("SGIQTP", "Failed to request inline results") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to request inline results") return .single(nil) } } |> map { contextResult -> SGIQTPResponse? in guard let contextResult, let firstResult = contextResult.results.first else { - SGLogger.shared.log("SGIQTP", "Empty inline result") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty inline result") return nil } @@ -70,7 +71,7 @@ public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults description: firstResult.description, text: t ) - SGLogger.shared.log("SGIQTP", "Response: \(response)") + SGLogger.shared.log("SGIQTP", "[\(queryId)] Response: \(response)") return response } } diff --git a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift index 32acfc1cc9..fb69b1ffe4 100644 --- a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift +++ b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift @@ -126,6 +126,7 @@ public class SGSimpleSettings { case inputToolbar case pinnedMessageNotifications case mentionsAndRepliesNotifications + case primaryUserId } public enum DownloadSpeedBoostValues: String, CaseIterable { @@ -236,7 +237,8 @@ public class SGSimpleSettings { Keys.confirmCalls.rawValue: true, Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue, Keys.messageFilterKeywords.rawValue: [], - Keys.inputToolbar.rawValue: false + Keys.inputToolbar.rawValue: false, + Keys.primaryUserId.rawValue: "" ] public static let groupDefaultValues: [String: Any] = [ @@ -437,6 +439,9 @@ public class SGSimpleSettings { @UserDefault(key: Keys.mentionsAndRepliesNotifications.rawValue, userDefaults: UserDefaults(suiteName: appGroupIdentifier) ?? .standard) public var mentionsAndRepliesNotifications: String + + @UserDefault(key: Keys.primaryUserId.rawValue) + public var primaryUserId: String } extension SGSimpleSettings { diff --git a/Swiftgram/SGStatus/BUILD b/Swiftgram/SGStatus/BUILD new file mode 100644 index 0000000000..acef413a33 --- /dev/null +++ b/Swiftgram/SGStatus/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGStatus", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGStatus/Sources/SGStatus.swift b/Swiftgram/SGStatus/Sources/SGStatus.swift new file mode 100644 index 0000000000..6bedd862a5 --- /dev/null +++ b/Swiftgram/SGStatus/Sources/SGStatus.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftSignalKit +import TelegramCore + +public struct SGStatus: Equatable, Codable { + public var status: Int64 + + public static var `default`: SGStatus { + return SGStatus(status: 1) + } + + public init(status: Int64) { + self.status = status + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.status = try container.decodeIfPresent(Int64.self, forKey: "status") ?? 1 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encodeIfPresent(self.status, forKey: "status") + } +} + +public func updateSGStatusInteractively(accountManager: AccountManager, _ f: @escaping (SGStatus) -> SGStatus) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.sgStatus, { entry in + let currentSettings: SGStatus + if let entry = entry?.get(SGStatus.self) { + currentSettings = entry + } else { + currentSettings = SGStatus.default + } + return SharedPreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift b/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift index 6690795465..0b804b5870 100644 --- a/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift +++ b/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift @@ -27,7 +27,17 @@ public func ignoreSignalErrors(onError: ((E) -> Void)? = nil) -> (Signal: Error { + public let error: E + + public init(_ error: E) { + self.error = error + } +} + +// Extension for Signals with Error types +extension Signal { @available(iOS 13.0, *) public func awaitable() async throws -> T { return try await withCheckedThrowingContinuation { continuation in @@ -38,7 +48,11 @@ extension Signal where E: Error { disposable?.dispose() }, error: { error in - continuation.resume(throwing: error) + if let error = error as? Error { + continuation.resume(throwing: error) + } else { + continuation.resume(throwing: SignalError(error)) + } disposable?.dispose() }, completed: { @@ -49,6 +63,7 @@ extension Signal where E: Error { } } +// Extension for Signals with NoError extension Signal where E == NoError { @available(iOS 13.0, *) public func awaitable() async -> T { @@ -71,6 +86,7 @@ extension Signal where E == NoError { } } +// Extension for general Signal types - AsyncStream support extension Signal { @available(iOS 13.0, *) public func awaitableStream() -> AsyncStream { @@ -94,7 +110,7 @@ extension Signal { } } - +// Extension for NoError Signal types - AsyncStream support extension Signal where E == NoError { @available(iOS 13.0, *) public func awaitableStream() -> AsyncStream { diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a1bc2f1818..19ecc29b16 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -933,6 +933,8 @@ public protocol SharedAccountContext: AnyObject { var automaticMediaDownloadSettings: Signal { get } var currentAutodownloadSettings: Atomic { get } var immediateExperimentalUISettings: ExperimentalUISettings { get } + // MARK: Swiftgram + var immediateSGStatus: SGStatus { get } var currentInAppNotificationSettings: Atomic { get } var currentMediaInputSettings: Atomic { get } var currentStickerSettings: Atomic { get } diff --git a/submodules/AccountUtils/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift index e15854a5f3..673ea24151 100644 --- a/submodules/AccountUtils/Sources/AccountUtils.swift +++ b/submodules/AccountUtils/Sources/AccountUtils.swift @@ -52,3 +52,18 @@ public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool } } } + +// MARK: Swiftgram +public func getContextForUserId(context: AccountContext, userId: Int64) -> Signal { + if context.account.peerId.id._internalGetInt64Value() == userId { + return .single(context) + } + return context.sharedContext.activeAccountContexts + |> take(1) + |> map { _, activeAccounts, _ -> AccountContext? in + if let account = activeAccounts.first(where: { $0.1.account.peerId.id._internalGetInt64Value() == userId }) { + return account.1 + } + return nil + } +} diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 0c955d674c..b06d74b40a 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -236,12 +236,12 @@ public final class InAppPurchaseManager: NSObject { super.init() - SKPaymentQueue.default().add(self) + // SKPaymentQueue.default().add(self) // MARK: Swiftgram self.requestProducts() } deinit { - SKPaymentQueue.default().remove(self) + // SKPaymentQueue.default().remove(self) // MARK: Swiftgram } var canMakePayments: Bool { @@ -249,6 +249,7 @@ public final class InAppPurchaseManager: NSObject { } private func requestProducts() { + if ({ return true }()) { return } // MARK: Swiftgram Logger.shared.log("InAppPurchaseManager", "Requesting products") let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers)) productRequest.delegate = self @@ -296,7 +297,7 @@ public final class InAppPurchaseManager: NSObject { let payment = SKMutablePayment(product: product.skProduct) payment.applicationUsername = accountPeerId payment.quantity = Int(quantity) - SKPaymentQueue.default().add(payment) + // SKPaymentQueue.default().add(payment) // MARK: Swiftgram let productIdentifier = payment.productIdentifier let signal = Signal { subscriber in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift index c72deb7f74..0ade702cb6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift @@ -31,6 +31,7 @@ public let SGLocalizations: [LocalizationInfo] = [ LocalizationInfo(languageCode: "zhcncc", baseLanguageCode: "zh-hans-raw", customPluralizationCode: "zh", title: "Chinese (Simplified) zhcncc", localizedTitle: "简体中文 (聪聪) - 已更完", isOfficial: true, totalStringCount: 7160, translatedStringCount: 7144, platformUrl: "https://translations.telegram.org/zhcncc/"), LocalizationInfo(languageCode: "taiwan", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (zh-Hant-TW) @zh_Hant_TW", localizedTitle: "正體中文", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3761, platformUrl: "https://translations.telegram.org/taiwan/"), LocalizationInfo(languageCode: "hongkong", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (Hong Kong)", localizedTitle: "中文(香港)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 6083, platformUrl: "https://translations.telegram.org/hongkong/"), + // TODO(swiftgram): Japanese beta // baseLanguageCode is actually nil, since it's an "official" beta language LocalizationInfo(languageCode: "vi-raw", baseLanguageCode: "vi-raw", customPluralizationCode: "vi", title: "Vietnamese", localizedTitle: "Tiếng Việt (beta)", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3795, platformUrl: "https://translations.telegram.org/vi/"), LocalizationInfo(languageCode: "hi-raw", baseLanguageCode: "hi-raw", customPluralizationCode: "hi", title: "Hindi", localizedTitle: "हिन्दी (beta)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 992, platformUrl: "https://translations.telegram.org/hi/"), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift index 05dc2ec2b0..ea151b836f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift @@ -52,7 +52,7 @@ public struct RequestChatContextResultsResult { } } -func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { +func _internal_requestChatContextResults(forIQTP: Bool = false, account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) { return (bot, peer) @@ -77,7 +77,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId return .single(nil) } - return account.postbox.transaction { transaction -> Signal in + return account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> Signal in var staleResult: RequestChatContextResultsResult? if offset.isEmpty && location == nil { @@ -127,6 +127,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location) } |> mapError { error -> RequestChatContextResultsError in + print("error.errorDescription", error.errorDescription ?? "") if error.errorDescription == "BOT_INLINE_GEO_REQUIRED" { return .locationRequired } else { @@ -138,7 +139,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId return .single(nil) } - return account.postbox.transaction { transaction -> RequestChatContextResultsResult? in + return account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> RequestChatContextResultsResult? in if result.cacheTimeout > 10, offset.isEmpty && location == nil { if let resultData = try? JSONEncoder().encode(result) { let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index c0acab773b..caacfe0ba6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -361,8 +361,8 @@ public extension TelegramEngine { return _internal_updateStarsReactionIsAnonymous(account: self.account, messageId: id, isAnonymous: isAnonymous) } - public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { - return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) + public func requestChatContextResults(forIQTP: Bool = false, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { + return _internal_requestChatContextResults(forIQTP: Bool = false, account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) } public func removeRecentlyUsedHashtag(string: String) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index b716f47008..89fc400c35 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -179,7 +179,7 @@ public extension TelegramEngine { return _internal_inactiveChannelList(network: self.account.network) } - public func resolvePeerByName(name: String, referrer: String?, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { + public func resolvePeerByName(forIQTP: Bool = false, name: String, referrer: String?, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { return _internal_resolvePeerByName(account: self.account, name: name, referrer: referrer, ageLimit: ageLimit) |> mapToSignal { result -> Signal in switch result { @@ -189,7 +189,7 @@ public extension TelegramEngine { guard let peerId = peerId else { return .single(.result(nil)) } - return self.account.postbox.transaction { transaction -> ResolvePeerResult in + return self.account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> ResolvePeerResult in return .result(transaction.getPeer(peerId).flatMap(EnginePeer.init)) } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 66e8642375..06237adcf6 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -18,6 +18,7 @@ sgdeps = [ "//Swiftgram/SGDeviceToken:SGDeviceToken", "//Swiftgram/SGDebugUI:SGDebugUI", "//Swiftgram/SGInputToolbar:SGInputToolbar", + "//Swiftgram/SGIAP:SGIAP", # "//Swiftgram/SGContentAnalysis:SGContentAnalysis" ] diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index ab0364f451..95586eb069 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1,4 +1,10 @@ // MARK: Swiftgram +import StoreKit +import SGIAP +import SGAPI +import SGDeviceToken +import SGAPIToken + import SGActionRequestHandlerSanitizer import SGAPIWebSettings import SGLogging @@ -1265,11 +1271,14 @@ private func extractAccountManagerState(records: AccountRecordsView Int { } return Int(info.phys_footprint) } + +// MARK: Swiftgram +@available(iOS 13.0, *) +extension AppDelegate { + + func setupIAP() { + NotificationCenter.default.addObserver(forName: .SGIAPHelperPurchaseNotification, object: nil, queue: nil) { [weak self] notification in + guard let strongSelf = self else { return } + if let transaction = notification.object as? SKPaymentTransaction { + let _ = (strongSelf.context.get() + |> take(1) + |> deliverOnMainQueue).start(next: { context in + guard let context = context else { + SGLogger.shared.log("SGIAP", "Empty app context (how?)") + + SGLogger.shared.log("SGIAP", "Finishing transaction") + SKPaymentQueue.default().finishTransaction(transaction) + return + } + let _ = Task { + await strongSelf.sendReceiptForVerification(primaryContext: context.context) + await strongSelf.fetchSGStatus(primaryContext: context.context) + SGLogger.shared.log("SGIAP", "Finishing transaction") + SKPaymentQueue.default().finishTransaction(transaction) + } + }) + } else { + SGLogger.shared.log("SGIAP", "Wrong object in SGIAPHelperPurchaseNotification") + } + } + } + + func getPrimaryContext(anyContext context: AccountContext, fallbackToCurrent: Bool = false) async -> AccountContext { + var primaryUserId: Int64 = Int64(SGSimpleSettings.shared.primaryUserId) ?? 0 + if primaryUserId == 0 { + primaryUserId = context.account.peerId.id._internalGetInt64Value() + SGLogger.shared.log("SGIAP", "Setting new primary user id: \(primaryUserId)") + SGSimpleSettings.shared.primaryUserId = String(primaryUserId) + } + + var primaryContext = await getContextForUserId(context: context, userId: primaryUserId).awaitable() + if let primaryContext = primaryContext { + SGLogger.shared.log("SGIAP", "Got primary context for user id: \(primaryContext.account.peerId.id._internalGetInt64Value())") + return primaryContext + } else { + primaryContext = context + let newPrimaryUserId = context.account.peerId.id._internalGetInt64Value() + SGLogger.shared.log("SGIAP", "Primary context for user id \(primaryUserId) is nil! Falling back to current context with user id: \(context.account.peerId.id._internalGetInt64Value())") + SGLogger.shared.log("SGIAP", "Setting new primary user id: \(primaryUserId)") + SGSimpleSettings.shared.primaryUserId = String(newPrimaryUserId) + return context + } + } + + func sendReceiptForVerification(primaryContext: AccountContext) async { + guard let receiptData = getPurchaceReceiptData() else { + return + } + + let encodedReceiptData = receiptData.base64EncodedData(options: []) + + var deviceToken: String? + var apiToken: String? + do { + async let deviceTokenTask = getDeviceToken().awaitable() + async let apiTokenTask = getSGApiToken(context: primaryContext).awaitable() + + (deviceToken, apiToken) = try await (deviceTokenTask, apiTokenTask) + } catch { + SGLogger.shared.log("SGIAP", "Error getting device token or API token: \(error)") + return + } + + if let deviceToken, let apiToken { + do { + let _ = try await postSGReceipt(token: apiToken, + deviceToken: deviceToken, + encodedReceiptData: encodedReceiptData).awaitable() + } catch { + SGLogger.shared.log("SGIAP", "Error: \(error)") + } + } + } + + func fetchSGStatus(primaryContext: AccountContext) async { +// let currentShouldKeepConnection = await (primaryContext.account.network.shouldKeepConnection.get() |> take(1) |> deliverOnMainQueue).awaitable() + let currentShouldKeepConnection = false + let userId = primaryContext.account.peerId.id._internalGetInt64Value() +// SGLogger.shared.log("SGIAP", "User id \(userId) currently keeps connection: \(currentShouldKeepConnection)") + if !currentShouldKeepConnection { + SGLogger.shared.log("SGIAP", "Asking user id \(userId) to keep connection: true") + primaryContext.account.network.shouldKeepConnection.set(.single(true)) + } + let iqtpResponse = await sgIqtpQuery(engine: primaryContext.engine, query: makeIqtpQuery(0, "s")).awaitable() + guard let iqtpResponse = iqtpResponse else { + SGLogger.shared.log("SGIAP", "IQTP response is nil!") +// if !currentShouldKeepConnection { +// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false") +// primaryContext.account.network.shouldKeepConnection.set(.single(false)) +// } + return + } + SGLogger.shared.log("SGIAP", "Got IQTP response: \(iqtpResponse)") + let _ = await updateSGStatusInteractively(accountManager: primaryContext.sharedContext.accountManager, { value in + var value = value + + let newStatus: Int64 + if let description = iqtpResponse.description, let status = Int64(description) { + newStatus = status + } else { + SGLogger.shared.log("SGIAP", "Can't parse IQTP response into status!") + newStatus = value.status // unparseable + } + + let userId = primaryContext.account.peerId.id._internalGetInt64Value() + if value.status != newStatus { + SGLogger.shared.log("SGIAP", "Updating \(userId) status \(value.status) -> \(newStatus)") + if newStatus > 1 { + SGSimpleSettings.shared.primaryUserId = String(userId) + } + value.status = newStatus + } else { + SGLogger.shared.log("SGIAP", "Status \(value.status) for \(userId) hasn't changed") + } + return value + }).awaitable() + +// if !currentShouldKeepConnection { +// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false") +// primaryContext.account.network.shouldKeepConnection.set(.single(false)) +// } + } +} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a79b34bd93..60e822cb28 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1,3 +1,5 @@ +// MARK: Swiftgram +import SGIAP import Foundation import UIKit import AsyncDisplayKit @@ -240,6 +242,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { return self.immediateExperimentalUISettingsValue.with { $0 } } private var experimentalUISettingsDisposable: Disposable? + + private var immediateSGStatusValue = Atomic(value: SGStatus.default) + public var immediateSGStatus: SGStatus { + return self.immediateSGStatusValue.with { $0 } + } + private var sgStatusDisposable: Disposable? + public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in } public var presentCrossfadeController: () -> Void = {} @@ -468,6 +477,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { let _ = immediateExperimentalUISettingsValue.swap(settings) } }) + // MARK: Swiftgram + let immediateSGStatusValue = self.immediateSGStatusValue + self.sgStatusDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus]) + |> deliverOnMainQueue).start(next: { sharedData in + if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) { + let _ = immediateSGStatusValue.swap(settings) + } + }) let _ = self.contactDataManager?.personNameDisplayOrder().start(next: { order in let _ = updateContactSettingsInteractively(accountManager: accountManager, { settings in @@ -3020,3 +3037,11 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation } return nil } + + +// MARK: Swiftgram +//public extension SharedAccountContextImpl { +// public func initSGIAP() { +// self.sgIAP = SGIAP +// } +//} diff --git a/submodules/TelegramUIPreferences/BUILD b/submodules/TelegramUIPreferences/BUILD index 5a106b25ca..e18637975c 100644 --- a/submodules/TelegramUIPreferences/BUILD +++ b/submodules/TelegramUIPreferences/BUILD @@ -1,9 +1,13 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgsrcs = [ + "//Swiftgram/SGStatus:SGStatus" +] + swift_library( name = "TelegramUIPreferences", module_name = "TelegramUIPreferences", - srcs = glob([ + srcs = sgsrcs + glob([ "Sources/**/*.swift", ]), copts = [ diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 55f080e9d0..992e8e65c5 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -21,6 +21,8 @@ public struct ApplicationSpecificPreferencesKeys { } private enum ApplicationSpecificSharedDataKeyValues: Int32 { + // MARK: Swiftgram + case sgStatus = 999 case inAppNotificationSettings = 0 case presentationPasscodeSettings = 1 case automaticMediaDownloadSettings = 2 @@ -45,6 +47,8 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { } public struct ApplicationSpecificSharedDataKeys { + // MARK: Swiftgram + public static let sgStatus = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.sgStatus.rawValue) public static let inAppNotificationSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.inAppNotificationSettings.rawValue) public static let presentationPasscodeSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.presentationPasscodeSettings.rawValue) public static let automaticMediaDownloadSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.automaticMediaDownloadSettings.rawValue)