From b22e91bdfa16862c801a34a62fbaad94b7c3db39 Mon Sep 17 00:00:00 2001 From: Kylmakalle Date: Mon, 10 Feb 2025 18:00:50 +0200 Subject: [PATCH] Handling State --- Swiftgram/SGDebugUI/Sources/SGDebugUI.swift | 1 + Swiftgram/SGIAP/Sources/SGIAP.swift | 3 +- Swiftgram/SGPayWall/Sources/SGPayWall.swift | 92 +++++++++---------- .../Sources/LocalizationManager.swift | 7 ++ Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift | 16 ++++ .../TelegramUI/Sources/AppDelegate.swift | 56 ++++++++--- 6 files changed, 112 insertions(+), 63 deletions(-) diff --git a/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift index 5619a5f93e..33b3592212 100644 --- a/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift +++ b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift @@ -1054,6 +1054,7 @@ public func sgDebugController(context: AccountContext) -> ViewController { let updateSettingsSignal = updateSGStatusInteractively(accountManager: context.sharedContext.accountManager, { status in var status = status status.status = SGStatus.default.status + SGSimpleSettings.shared.primaryUserId = "" return status }) let _ = (updateSettingsSignal |> deliverOnMainQueue).start(next: { diff --git a/Swiftgram/SGIAP/Sources/SGIAP.swift b/Swiftgram/SGIAP/Sources/SGIAP.swift index 00a42bb7ba..c2940161b4 100644 --- a/Swiftgram/SGIAP/Sources/SGIAP.swift +++ b/Swiftgram/SGIAP/Sources/SGIAP.swift @@ -91,6 +91,7 @@ public extension Notification.Name { static let SGIAPHelperPurchaseNotification = Notification.Name("SGIAPPurchaseNotification") static let SGIAPHelperErrorNotification = Notification.Name("SGIAPErrorNotification") static let SGIAPHelperProductsUpdatedNotification = Notification.Name("SGIAPProductsUpdatedNotification") + static let SGIAPHelperValidationErrorNotification = Notification.Name("SGIAPValidationErrorNotification") } public final class SGIAPManager: NSObject { @@ -208,7 +209,7 @@ public final class SGIAPManager: NSObject { SKPaymentQueue.default().add(self) - #if DEBUG + #if DEBUG && false DispatchQueue.main.asyncAfter(deadline: .now() + 20) { self.requestProducts() } diff --git a/Swiftgram/SGPayWall/Sources/SGPayWall.swift b/Swiftgram/SGPayWall/Sources/SGPayWall.swift index c88000d89f..cc53923833 100644 --- a/Swiftgram/SGPayWall/Sources/SGPayWall.swift +++ b/Swiftgram/SGPayWall/Sources/SGPayWall.swift @@ -30,7 +30,7 @@ public func sgPayWallController(statusSignal: Signal, replacemen let swiftUIView = SGSwiftUIView( legacyController: legacyController, content: { - SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, lang: strings.baseLanguageCode) + SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal) } ) let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) @@ -99,35 +99,36 @@ struct BackgroundView: View { struct SGPayWallView: View { @Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat @Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout? + @Environment(\.lang) var lang: String weak var wrapperController: LegacyController? let replacementController: ViewController let SGIAP: SGIAPManager let statusSignal: Signal - let lang: String private enum PayWallState: Equatable { case ready // ready to buy case restoring case purchasing case validating - case purchaseError(String) // error purchasing } // State management @State private var product: SGIAPManager.SGProduct? @State private var currentStatus: Int64 = 1 @State private var state: PayWallState = .ready + @State private var showErrorAlert: Bool = false @State private var showConfetti: Bool = false private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil) - private let buySuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil) + private let buyOrRestoreSuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil) private let buyErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperErrorNotification, object: nil) + private let validationErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperValidationErrorNotification, object: nil) @State private var statusTask: Task? = nil @State private var hapticFeedback: HapticFeedback? - private let confettiDuration: Double = 7.0 + private let confettiDuration: Double = 5.0 var body: some View { ZStack { @@ -146,7 +147,7 @@ struct SGPayWallView: View { .font(.largeTitle) .fontWeight(.bold) - Text("Supercharged with Pro features") + Text("Supercharged with Pro features".i18n(lang)) .font(.callout) .multilineTextAlignment(.center) .padding(.horizontal) @@ -179,9 +180,9 @@ struct SGPayWallView: View { updateSelectedProduct() statusTask = Task { let statusStream = statusSignal.awaitableStream() - for await status in statusStream { + for await newStatus in statusStream { #if DEBUG - print("SGPayWallView: status = \(status)") + print("SGPayWallView: newStatus = \(newStatus)") #endif if Task.isCancelled { #if DEBUG @@ -190,10 +191,13 @@ struct SGPayWallView: View { break } - if currentStatus != status && status > 1 { - handleUpgradedStatus() + if currentStatus != newStatus { + currentStatus = newStatus + + if newStatus > 1 { + handleUpgradedStatus() + } } - currentStatus = status } } } @@ -203,36 +207,22 @@ struct SGPayWallView: View { #endif statusTask?.cancel() } - .onReceive(buySuccessPub) { _ in + .onReceive(buyOrRestoreSuccessPub) { _ in state = .validating } .onReceive(buyErrorPub) { notification in if let userInfo = notification.userInfo, let error = userInfo["localizedError"] as? String, !error.isEmpty { - state = .purchaseError(error) - } else { - state = .ready + showErrorAlert(error) } } - .alert(isPresented: Binding(get: { - if case .purchaseError = state { - return true + .onReceive(validationErrorPub) { notification in + if state == .validating { + if let userInfo = notification.userInfo, let error = userInfo["error"] as? String, !error.isEmpty { + showErrorAlert(error) + } else { + showErrorAlert("Validation Error") } - return false - }, - set: { _ in }) - ) { - Alert( - title: Text("Error"), - message: { - if case .purchaseError(let message) = state { - return Text(message) - } - return Text("") - }(), - dismissButton: .default(Text("OK"), action: { - state = .ready - }) - ) + } } } @@ -273,8 +263,8 @@ struct SGPayWallView: View { .fontWeight(.semibold) .foregroundColor(Color(hex: accentColorHex)) } - .disabled(state == .restoring) - .opacity(state == .restoring ? 0.5 : 1.0) + .disabled(state == .restoring || product == nil) + .opacity((state == .restoring || product == nil) ? 0.5 : 1.0) } private var purchaseSection: some View { @@ -301,7 +291,9 @@ struct SGPayWallView: View { } private var closeButtonView: some View { - Button(action: dismiss) { + Button(action: { + wrapperController?.dismiss(animated: true) + }) { Image(systemName: "xmark") .font(.headline) .foregroundColor(.secondary.opacity(0.6)) @@ -311,25 +303,25 @@ struct SGPayWallView: View { .padding([.top, .trailing], 16) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) } - + private var buttonTitle: String { if currentStatus > 1 { - return "Use Pro features" + return "Use Pro features".i18n(lang) } else { if state == .purchasing { - return "Purchasing..." + return "Purchasing...".i18n(lang) } else if state == .restoring { - return "Restoring Purchases..." + return "Restoring Purchases...".i18n(lang) } else if state == .validating { - return "Validating Purchase..." + return "Validating Purchase...".i18n(lang) } else if let product = product { if !SGIAP.canMakePayments { - return "Payments unavailable" + return "Payments unavailable".i18n(lang) } else { - return "Subscribe for \(product.price) / month" + return "Subscribe for \(product.price) / month".i18n(lang, args: product.price) } } else { - return "Contacting App Store..." + return "Contacting App Store...".i18n(lang) } } } @@ -373,8 +365,14 @@ struct SGPayWallView: View { } } - private func dismiss() { - wrapperController?.dismiss(animated: true) + private func showErrorAlert(_ message: String) { + let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in + state = .ready + })) + DispatchQueue.main.async { + wrapperController?.present(alertController, animated: true) + } } } diff --git a/Swiftgram/SGStrings/Sources/LocalizationManager.swift b/Swiftgram/SGStrings/Sources/LocalizationManager.swift index 51c646ceb3..bbaef436ac 100644 --- a/Swiftgram/SGStrings/Sources/LocalizationManager.swift +++ b/Swiftgram/SGStrings/Sources/LocalizationManager.swift @@ -119,3 +119,10 @@ public class SGLocalizationManager { } public let i18n = SGLocalizationManager.shared.localizedString + + +public extension String { + func i18n(_ locale: String = SGFallbackLocale, args: CVarArg...) -> String { + return SGLocalizationManager.shared.localizedString(self, locale, args: args) + } +} diff --git a/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift index 1dcf195265..b5ee5964ef 100644 --- a/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift +++ b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift @@ -24,6 +24,11 @@ public struct ContainerViewLayoutKey: EnvironmentKey { public static let defaultValue: ContainerViewLayout? = nil } +@available(iOS 13.0, *) +public struct LangKey: EnvironmentKey { + public static let defaultValue: String = "en" +} + // Perhaps, affects Performance a lot //@available(iOS 13.0, *) //public struct ContainerViewLayoutUpdateCountKey: EnvironmentKey { @@ -41,6 +46,11 @@ public extension EnvironmentValues { get { self[ContainerViewLayoutKey.self] } set { self[ContainerViewLayoutKey.self] = newValue } } + + var lang: String { + get { self[LangKey.self] } + set { self[LangKey.self] = newValue } + } // var containerViewLayoutUpdateCount: ObservedValue { // get { self[ContainerViewLayoutUpdateCountKey.self] } @@ -58,6 +68,8 @@ public struct SGSwiftUIView: View { @ObservedObject var containerViewLayout: ObservedValue // @ObservedObject var containerViewLayoutUpdateCount: ObservedValue + private var lang: String + public init( legacyController: LegacySwiftUIController, manageSafeArea: Bool = false, @@ -70,6 +82,7 @@ public struct SGSwiftUIView: View { #endif self.navigationBarHeight = legacyController.navigationBarHeightModel self.containerViewLayout = legacyController.containerViewLayoutModel + self.lang = legacyController.lang // self.containerViewLayoutUpdateCount = legacyController.containerViewLayoutUpdateCountModel self.manageSafeArea = manageSafeArea self.content = content() @@ -80,6 +93,7 @@ public struct SGSwiftUIView: View { .if(manageSafeArea) { $0.modifier(CustomSafeArea()) } .environment(\.navigationBarHeight, navigationBarHeight.value) .environment(\.containerViewLayout, containerViewLayout.value) + .environment(\.lang, lang) // .environment(\.containerViewLayoutUpdateCount, containerViewLayoutUpdateCount) // .onReceive(containerViewLayoutUpdateCount.$value) { _ in // // Make sure View is updated when containerViewLayoutUpdateCount changes, @@ -155,12 +169,14 @@ public final class LegacySwiftUIController: LegacyController { public var navigationBarHeightModel: ObservedValue public var containerViewLayoutModel: ObservedValue public var inputHeightModel: ObservedValue + public let lang: String // public var containerViewLayoutUpdateCountModel: ObservedValue override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { navigationBarHeightModel = ObservedValue(0.0) containerViewLayoutModel = ObservedValue(initialLayout) inputHeightModel = ObservedValue(nil) + lang = strings?.baseLanguageCode ?? "en" // containerViewLayoutUpdateCountModel = ObservedValue(0) super.init(presentation: presentation, theme: theme, strings: strings, initialLayout: initialLayout) } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index a7ac1126e2..92029fa5a5 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -3075,27 +3075,45 @@ extension AppDelegate { NotificationCenter.default.addObserver(forName: .SGIAPHelperPurchaseNotification, object: nil, queue: nil) { [weak self] notification in SGLogger.shared.log("SGIAP", "Got SGIAPHelperPurchaseNotification") guard let strongSelf = self else { return } - if let transaction = notification.object as? SKPaymentTransaction { + if let transactions = notification.object as? [SKPaymentTransaction] { let _ = (strongSelf.context.get() |> take(1) - |> deliverOnMainQueue).start(next: { context in - SGLogger.shared.log("SGIAP", "Got context for SGIAPHelperPurchaseNotification") + |> deliverOnMainQueue).start(next: { [weak strongSelf] context in + guard let veryStrongSelf = strongSelf else { + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } + return + } guard let context = context else { SGLogger.shared.log("SGIAP", "Empty app context (how?)") - SGLogger.shared.log("SGIAP", "Finishing transaction \(transaction.transactionIdentifier ?? "nil")") - SKPaymentQueue.default().finishTransaction(transaction) + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } return } + SGLogger.shared.log("SGIAP", "Got context for SGIAPHelperPurchaseNotification") let _ = Task { - await strongSelf.sendReceiptForVerification(primaryContext: context.context) - await strongSelf.fetchSGStatus(primaryContext: context.context) - SGLogger.shared.log("SGIAP", "Finishing transaction \(transaction.transactionIdentifier ?? "nil")") - SKPaymentQueue.default().finishTransaction(transaction) + await veryStrongSelf.sendReceiptForVerification(primaryContext: context.context) + await veryStrongSelf.fetchSGStatus(primaryContext: context.context) + + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } } }) } else { SGLogger.shared.log("SGIAP", "Wrong object in SGIAPHelperPurchaseNotification") + #if DEBUG + preconditionFailure("Wrong object in SGIAPHelperPurchaseNotification") + #endif } } } @@ -3104,8 +3122,6 @@ extension AppDelegate { 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 = try? await getContextForUserId(context: context, userId: primaryUserId).awaitable() @@ -3115,9 +3131,7 @@ extension AppDelegate { } 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) + SGLogger.shared.log("SGIAP", "Primary context for user id \(primaryUserId) is nil! Falling back to current context with user id: \(newPrimaryUserId)") return context } } @@ -3172,6 +3186,9 @@ extension AppDelegate { // SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false") // primaryContext.account.network.shouldKeepConnection.set(.single(false)) // } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .SGIAPHelperValidationErrorNotification, object: nil, userInfo: ["error": "PayWall.ValidationError.TryAgain"]) + } return } SGLogger.shared.log("SGIAP", "Got IQTP response: \(iqtpResponse)") @@ -3190,11 +3207,20 @@ extension AppDelegate { if value.status != newStatus { SGLogger.shared.log("SGIAP", "Updating \(userId) status \(value.status) -> \(newStatus)") if newStatus > 1 { - SGSimpleSettings.shared.primaryUserId = String(userId) + let stringUserId = String(userId) + if SGSimpleSettings.shared.primaryUserId != stringUserId { + SGLogger.shared.log("SGIAP", "Setting new primary user id: \(userId)") + SGSimpleSettings.shared.primaryUserId = stringUserId + } } value.status = newStatus } else { SGLogger.shared.log("SGIAP", "Status \(value.status) for \(userId) hasn't changed") + if newStatus < 1 { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .SGIAPHelperValidationErrorNotification, object: nil, userInfo: ["error": "PayWall.ValidationError.Expired"]) + } + } } return value }).awaitable()