diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift index 707e7328c4..155bf728a1 100644 --- a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift +++ b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift @@ -11,8 +11,17 @@ struct MembershipCoordinator: View { } var body: some View { - MembershipModuleView(userMembershipPublisher: model.$userMembership.eraseToAnyPublisher()) { tier in - model.onTierSelected(tier: tier) + Group { + if model.showTiersLoadingError { + errorView + } else { + MembershipModuleView( + membership: model.userMembership, + tiers: model.tiers + ) { tier in + model.onTierSelected(tier: tier) + } + } } .animation(.default, value: model.userMembership) @@ -28,6 +37,17 @@ struct MembershipCoordinator: View { } } + var errorView: some View { + EmptyStateView( + title: Loc.Error.Common.title, + subtitle: Loc.Error.Common.message, + actionText: Loc.Error.Common.tryAgain + ) { + model.loadTiers(noCache: false) + } + } + + func tierSelection(tier: MembershipTierId) -> some View { MembershipTierSelectionView(userMembership: model.userMembership, tierToDisplay: tier) { data in model.onEmailDataSubmit(data: data) diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift index a7d4ed4856..27762afa3d 100644 --- a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift +++ b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift @@ -5,12 +5,16 @@ import Services @MainActor final class MembershipCoordinatorModel: ObservableObject { @Published var userMembership: MembershipStatus = .empty + @Published var tiers: [MembershipTier] = [] + @Published var showTiersLoadingError = false @Published var showTier: MembershipTierId? @Published var showSuccess: MembershipTierId? @Published var emailVerificationData: EmailVerificationData? @Published var emailUrl: URL? + @Injected(\.membershipService) + private var membershipService: MembershipServiceProtocol @Injected(\.membershipStatusStorage) private var membershipStatusStorage: MembershipStatusStorageProtocol @Injected(\.accountManager) @@ -18,6 +22,7 @@ final class MembershipCoordinatorModel: ObservableObject { init() { membershipStatusStorage.status.assign(to: &$userMembership) + loadTiers(noCache: false) } func onTierSelected(tier: MembershipTierId) { @@ -42,7 +47,7 @@ final class MembershipCoordinatorModel: ObservableObject { func onSuccessfulValidation() { emailVerificationData = nil showTier = nil - + loadTiers(noCache: true) // https://linear.app/anytype/issue/IOS-2434/bottom-sheet-nesting Task { @@ -50,4 +55,15 @@ final class MembershipCoordinatorModel: ObservableObject { showSuccess = .explorer } } + + func loadTiers(noCache: Bool) { + Task { + do { + tiers = try await membershipService.getTiers(noCache: noCache) + showTiersLoadingError = false + } catch { + showTiersLoadingError = true + } + } + } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleView.swift index d32cc10e82..2c810b5a70 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleView.swift @@ -7,14 +7,17 @@ struct MembershipModuleView: View { @StateObject private var model: MembershipModuleViewModel @Environment(\.openURL) private var openURL + private let membership: MembershipStatus + private let tiers: [MembershipTier] + init( - userMembershipPublisher: AnyPublisher, + membership: MembershipStatus, + tiers: [MembershipTier], onTierTap: @escaping (MembershipTierId) -> () ) { - _model = StateObject(wrappedValue: MembershipModuleViewModel( - userMembershipPublisher: userMembershipPublisher, - onTierTap: onTierTap - )) + self.membership = membership + self.tiers = tiers + _model = StateObject(wrappedValue: MembershipModuleViewModel(onTierTap: onTierTap)) } var body: some View { @@ -32,7 +35,7 @@ struct MembershipModuleView: View { Spacer.fixedHeight(32) baners - MembershipTierListView(userMembership: model.userMembership) { + MembershipTierListView(userMembership: membership, tiers: tiers) { UISelectionFeedbackGenerator().selectionChanged() model.onTierTap(tier: $0) } @@ -114,7 +117,8 @@ struct MembershipModuleView: View { #Preview { NavigationView { MembershipModuleView( - userMembershipPublisher: .empty(), + membership: .empty, + tiers: [], onTierTap: { _ in } ) } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleViewModel.swift index 9f6991c74b..67a45fb6ea 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/MembershipModuleViewModel.swift @@ -4,17 +4,11 @@ import Combine @MainActor -final class MembershipModuleViewModel: ObservableObject { - @Published var userMembership: MembershipStatus = .empty - +final class MembershipModuleViewModel: ObservableObject { private let onTierTap: (MembershipTierId) -> () - init( - userMembershipPublisher: AnyPublisher, - onTierTap: @escaping (MembershipTierId) -> () - ) { + init(onTierTap: @escaping (MembershipTierId) -> ()) { self.onTierTap = onTierTap - userMembershipPublisher.assign(to: &$userMembership) } func onTierTap(tier: MembershipTierId) { diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTierListView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTierListView.swift index bc8421f67c..726c21290b 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTierListView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTierListView.swift @@ -4,6 +4,7 @@ import Services struct MembershipTierListView: View { let userMembership: MembershipStatus + let tiers: [MembershipTier] let onTierTap: (MembershipTierId) -> () var body: some View { @@ -12,7 +13,7 @@ struct MembershipTierListView: View { HStack(spacing: 20) { Spacer.fixedWidth(0) - ForEach(userMembership.tierId.availableTiers) { tier in + ForEach(tiers.map { $0.id }) { tier in MembershipTeirView(tierToDisplay: tier, userMembership: userMembership) { onTierTap(tier) } @@ -39,7 +40,12 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .explorer), + MembershipTier(id: .builder), + MembershipTier(id: .coCreator) + ] ) { _ in } MembershipTierListView( @@ -49,7 +55,12 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .explorer), + MembershipTier(id: .builder), + MembershipTier(id: .coCreator), + ] ) { _ in } MembershipTierListView( @@ -59,7 +70,12 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .explorer), + MembershipTier(id: .builder), + MembershipTier(id: .coCreator) + ] ) { _ in } MembershipTierListView( @@ -69,7 +85,12 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .custom(id: 0)), + MembershipTier(id: .builder), + MembershipTier(id: .coCreator) + ] ) { _ in } @@ -80,7 +101,11 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .builder), + MembershipTier(id: .coCreator) + ] ) { _ in } MembershipTierListView( @@ -90,7 +115,10 @@ struct MembershipTierListView: View { dateEnds: .tomorrow, paymentMethod: .methodCard, anyName: "" - ) + ), + tiers: [ + MembershipTier(id: .coCreator) + ] ) { _ in } } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift index 08bf453e4f..3a1bf46186 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift @@ -3,26 +3,6 @@ import Services import AnytypeCore -extension Optional where Wrapped == MembershipTierId { - var availableTiers: [MembershipTierId] { - switch self { - case .none: - [.explorer, .builder, .coCreator ] - case .some(let tier): - switch tier { - case .custom(let id): - [.custom(id: id), .builder, .coCreator ] - case .explorer: - [.explorer, .builder, .coCreator ] - case .builder: - [.builder, .coCreator ] - case .coCreator: - [.coCreator ] - } - } - } -} - extension MembershipTierId { var title: String { switch self { diff --git a/Modules/Services/Sources/Services/Membership/MembershipService.swift b/Modules/Services/Sources/Services/Membership/MembershipService.swift index 43b23e41cf..9f0dc7cd1b 100644 --- a/Modules/Services/Sources/Services/Membership/MembershipService.swift +++ b/Modules/Services/Sources/Services/Membership/MembershipService.swift @@ -4,6 +4,8 @@ import Foundation public protocol MembershipServiceProtocol { func getStatus() async throws -> MembershipStatus + func getTiers(noCache: Bool) async throws -> [MembershipTier] + func getVerificationEmail(data: EmailVerificationData) async throws func verifyEmailCode(code: String) async throws @@ -36,48 +38,12 @@ final class MembershipService: MembershipServiceProtocol { $0.requestedTier = tier.middlewareId }).invoke(ignoreLogErrors: .hasInvalidChars, .tooLong, .tooShort) } -} - -// MARK: - Models -public extension Anytype_Model_Membership { - func asModel() -> MembershipStatus { - MembershipStatus( - tierId: MembershipTierId, - status: status, - dateEnds: Date(timeIntervalSince1970: TimeInterval(dateEnds)), - paymentMethod: paymentMethod, - anyName: requestedAnyName - ) - } - var MembershipTierId: MembershipTierId? { - switch tier { - case 0: - nil - case 1: - .explorer - case 4: - .builder - case 5: - .coCreator - default: - .custom(id: tier) - } - } -} - -// TODO: Use API -extension MembershipTierId { - var middlewareId: Int32 { - switch self { - case .explorer: - 1 - case .builder: - 4 - case .coCreator: - 5 - case .custom(let id): - id - } + public func getTiers(noCache: Bool) async throws -> [MembershipTier] { + return try await ClientCommands.membershipGetTiers(.with { + $0.locale = Locale.current.languageCode ?? "en" + $0.noCache = noCache + }) + .invoke().tiers.filter { !$0.isTest }.compactMap { $0.asModel() } } } diff --git a/Modules/Services/Sources/Services/Membership/Model/MembershipStatus.swift b/Modules/Services/Sources/Services/Membership/Model/MembershipStatus.swift index 789d207983..8406ee30d6 100644 --- a/Modules/Services/Sources/Services/Membership/Model/MembershipStatus.swift +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipStatus.swift @@ -4,17 +4,6 @@ import Foundation public typealias MembershipSubscriptionStatus = Anytype_Model_Membership.Status public typealias MembershipPaymentMethod = Anytype_Model_Membership.PaymentMethod -public enum MembershipTierId: Hashable, Identifiable { - case explorer - case builder - case coCreator - - case custom(id: Int32) - - public var id: Self { - return self - } -} public struct MembershipStatus: Equatable { public let tierId: MembershipTierId? @@ -38,3 +27,17 @@ public struct MembershipStatus: Equatable { } } + +// MARK: - Middleware model mapping + +public extension Anytype_Model_Membership { + func asModel() -> MembershipStatus { + MembershipStatus( + tierId: MembershipTierId(intId: tier), + status: status, + dateEnds: Date(timeIntervalSince1970: TimeInterval(dateEnds)), + paymentMethod: paymentMethod, + anyName: requestedAnyName + ) + } +} diff --git a/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift b/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift new file mode 100644 index 0000000000..6cb1bf705a --- /dev/null +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift @@ -0,0 +1,64 @@ +import ProtobufMessages + + +public enum MembershipTierId: Hashable, Identifiable { + case explorer + case builder + case coCreator + + case custom(id: Int32) + + public var id: Self { + return self + } + + init?(intId: Int32) { + switch intId { + case 0: + return nil + case 1: + self = .explorer + case 4: + self = .builder + case 5: + self = .coCreator + default: + self = .custom(id: Int32(intId)) + } + } +} + +public struct MembershipTier { + public let id: MembershipTierId + + public init(id: MembershipTierId) { + self.id = id + } +} + + +// MARK: - Middleware model mapping + +extension Anytype_Model_MembershipTierData { + func asModel() -> MembershipTier? { + guard let tierId = MembershipTierId(intId: Int32(id)) else { return nil } + + return MembershipTier(id: tierId) + } +} + +// TODO: Use API +extension MembershipTierId { + var middlewareId: Int32 { + switch self { + case .explorer: + 1 + case .builder: + 4 + case .coCreator: + 5 + case .custom(let id): + id + } + } +}