Handling State

This commit is contained in:
Kylmakalle 2025-02-10 18:00:50 +02:00
parent 7e999e8c48
commit b22e91bdfa
6 changed files with 112 additions and 63 deletions

View File

@ -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: {

View File

@ -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()
}

View File

@ -30,7 +30,7 @@ public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacemen
let swiftUIView = SGSwiftUIView<SGPayWallView>(
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<Int64, NoError>
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<Void, Never>? = 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)
}
}
}

View File

@ -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)
}
}

View File

@ -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<Int64> {
// get { self[ContainerViewLayoutUpdateCountKey.self] }
@ -58,6 +68,8 @@ public struct SGSwiftUIView<Content: View>: View {
@ObservedObject var containerViewLayout: ObservedValue<ContainerViewLayout?>
// @ObservedObject var containerViewLayoutUpdateCount: ObservedValue<Int64>
private var lang: String
public init(
legacyController: LegacySwiftUIController,
manageSafeArea: Bool = false,
@ -70,6 +82,7 @@ public struct SGSwiftUIView<Content: View>: 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<Content: View>: 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<CGFloat>
public var containerViewLayoutModel: ObservedValue<ContainerViewLayout?>
public var inputHeightModel: ObservedValue<CGFloat?>
public let lang: String
// public var containerViewLayoutUpdateCountModel: ObservedValue<Int64>
override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
navigationBarHeightModel = ObservedValue<CGFloat>(0.0)
containerViewLayoutModel = ObservedValue<ContainerViewLayout?>(initialLayout)
inputHeightModel = ObservedValue<CGFloat?>(nil)
lang = strings?.baseLanguageCode ?? "en"
// containerViewLayoutUpdateCountModel = ObservedValue<Int64>(0)
super.init(presentation: presentation, theme: theme, strings: strings, initialLayout: initialLayout)
}

View File

@ -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()