From 55bc1d93b6f1625c8507f884a46594fa5cabb60f Mon Sep 17 00:00:00 2001 From: Vova Ignatov Date: Wed, 3 Apr 2024 21:57:08 +0100 Subject: [PATCH 1/4] IOS-2594 Use price data from StoreKit and Middelware --- Anytype.xcodeproj/project.pbxproj | 14 ++- .../xcshareddata/xcschemes/Anytype.xcscheme | 3 + Anytype/Generated/Strings.swift | 24 +++- .../Strings/en.lproj/Localizable.strings | 11 +- .../Helpers/Extensions/StringExtensions.swift | 19 ++++ .../MembershipCoordinator.swift | 2 +- .../Views/MembershipPricingView.swift | 24 ++++ .../Views/MembershipTeirView.swift | 17 +-- .../Models/MembershipTierExtensions.swift | 12 +- .../MembershipTierPaymentTypeExtension.swift | 106 ++++++++++++++++++ .../LocalStoreKitConfiguration.storekit | 99 ++++++++++++++++ .../AnytypeCore/Utils}/AsyncHelpers.swift | 2 +- .../Membership/MembershipService.swift | 53 ++++++++- .../Membership/Model/MembershipTier.swift | 23 +--- .../Model/MembershipTierPaymentType.swift | 27 +++++ 15 files changed, 387 insertions(+), 49 deletions(-) create mode 100644 Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift create mode 100644 Anytype/Supporting files/LocalStoreKitConfiguration.storekit rename {Anytype/Sources/Utilities => Modules/AnytypeCore/AnytypeCore/Utils}/AsyncHelpers.swift (94%) create mode 100644 Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift diff --git a/Anytype.xcodeproj/project.pbxproj b/Anytype.xcodeproj/project.pbxproj index 3c3638cd49..cfd6dbe5c2 100644 --- a/Anytype.xcodeproj/project.pbxproj +++ b/Anytype.xcodeproj/project.pbxproj @@ -407,7 +407,6 @@ 2A39F3CB29ED97F3005DE8F5 /* BlockTextStyle+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A39F3CA29ED97F2005DE8F5 /* BlockTextStyle+Analytics.swift */; }; 2A39F3CD29ED9A7E005DE8F5 /* LayoutAlignment+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A39F3CC29ED9A7E005DE8F5 /* LayoutAlignment+Analytics.swift */; }; 2A3AFF552A7CEB9F004F7E09 /* SpaceAccessibility+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3AFF542A7CEB9F004F7E09 /* SpaceAccessibility+Localization.swift */; }; - 2A4063632A5D480E00ECEA79 /* AsyncHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4063622A5D480E00ECEA79 /* AsyncHelpers.swift */; }; 2A4063652A5D9D8A00ECEA79 /* EventBunchSubscribtion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4063642A5D9D8A00ECEA79 /* EventBunchSubscribtion.swift */; }; 2A40F59F2BB1997D00F0220E /* PresentedDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A40F59E2BB1997D00F0220E /* PresentedDismiss.swift */; }; 2A41A6782B84E57B00EAE6E6 /* DeepLinks in Frameworks */ = {isa = PBXBuildFile; productRef = 2A41A6772B84E57B00EAE6E6 /* DeepLinks */; }; @@ -1382,6 +1381,8 @@ 3DE0CFE126F860EF003A606A /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE0CFE026F860EF003A606A /* StringExtension.swift */; }; 3DE296472617439C00EADB0A /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE296462617439C00EADB0A /* ServiceLocator.swift */; }; 3DE3C91D26F464BF0012AF51 /* MentionObjectsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE3C91926F464BF0012AF51 /* MentionObjectsService.swift */; }; + 3DE3DE652BBDBAA300C9E6D3 /* MembershipPricingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE3DE642BBDBAA300C9E6D3 /* MembershipPricingView.swift */; }; + 3DE3DE672BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE3DE662BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift */; }; 3DE5121626728C0F00153962 /* BaseDocumentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE5121526728C0F00153962 /* BaseDocumentProtocol.swift */; }; 3DE6924F27301C3E00A4CD4E /* AccessoryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE6924E27301C3E00A4CD4E /* AccessoryViewType.swift */; }; 3DE6926827302D5800A4CD4E /* AccessoryViewStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE6926727302D5800A4CD4E /* AccessoryViewStateManager.swift */; }; @@ -2205,7 +2206,6 @@ 2A39F3CA29ED97F2005DE8F5 /* BlockTextStyle+Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlockTextStyle+Analytics.swift"; sourceTree = ""; }; 2A39F3CC29ED9A7E005DE8F5 /* LayoutAlignment+Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LayoutAlignment+Analytics.swift"; sourceTree = ""; }; 2A3AFF542A7CEB9F004F7E09 /* SpaceAccessibility+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SpaceAccessibility+Localization.swift"; sourceTree = ""; }; - 2A4063622A5D480E00ECEA79 /* AsyncHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelpers.swift; sourceTree = ""; }; 2A4063642A5D9D8A00ECEA79 /* EventBunchSubscribtion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBunchSubscribtion.swift; sourceTree = ""; }; 2A40F59E2BB1997D00F0220E /* PresentedDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentedDismiss.swift; sourceTree = ""; }; 2A41A6762B84D43C00EAE6E6 /* DeepLinks */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DeepLinks; path = Modules/DeepLinks; sourceTree = ""; }; @@ -3175,6 +3175,9 @@ 3DE0CFE026F860EF003A606A /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 3DE296462617439C00EADB0A /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = ""; }; 3DE3C91926F464BF0012AF51 /* MentionObjectsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionObjectsService.swift; sourceTree = ""; }; + 3DE3DE632BBD7E6F00C9E6D3 /* LocalStoreKitConfiguration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = LocalStoreKitConfiguration.storekit; sourceTree = ""; }; + 3DE3DE642BBDBAA300C9E6D3 /* MembershipPricingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipPricingView.swift; sourceTree = ""; }; + 3DE3DE662BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipTierPaymentTypeExtension.swift; sourceTree = ""; }; 3DE5121526728C0F00153962 /* BaseDocumentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BaseDocumentProtocol.swift; path = Anytype/Sources/Models/Documents/Document/BaseDocumentProtocol.swift; sourceTree = SOURCE_ROOT; }; 3DE6924E27301C3E00A4CD4E /* AccessoryViewType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryViewType.swift; sourceTree = ""; }; 3DE6926727302D5800A4CD4E /* AccessoryViewStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryViewStateManager.swift; sourceTree = ""; }; @@ -3761,6 +3764,7 @@ 038FB5C522D8F2A0002B90B8 /* Supporting files */ = { isa = PBXGroup; children = ( + 3DE3DE632BBD7E6F00C9E6D3 /* LocalStoreKitConfiguration.storekit */, 0303ED0922D8EDAD005C552B /* LaunchScreen.storyboard */, 0303ED0C22D8EDAD005C552B /* Info.plist */, ); @@ -4189,7 +4193,6 @@ 1270CE8E26DA848B00B2D443 /* Images */, EAC5FD4A2657DC3D006BB24E /* UIScrollView */, 0A996BD023F0342A000FDC9C /* PlistReader.swift */, - 2A4063622A5D480E00ECEA79 /* AsyncHelpers.swift */, 2AEDACC32B9F3A4600FA757A /* AppContext.swift */, ); path = Utilities; @@ -8705,6 +8708,7 @@ children = ( 3D611E952B93779900B33305 /* MembershipTierExtensions.swift */, 3DFD50A32BA0B114002E51F7 /* MembershipStatusExtension.swift */, + 3DE3DE662BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift */, ); path = Models; sourceTree = ""; @@ -9198,6 +9202,7 @@ 3DD0CEBA2B90E6BB00657402 /* MembershipBannerView.swift */, 3D611E8D2B936B8300B33305 /* MembershipTeirView.swift */, 3D611E922B93775B00B33305 /* MembershipTierListView.swift */, + 3DE3DE642BBDBAA300C9E6D3 /* MembershipPricingView.swift */, ); path = Views; sourceTree = ""; @@ -10837,6 +10842,7 @@ 3D26AA7E2B9768F500D61479 /* EmailVerificationView.swift in Sources */, 3D22B20E2685DABD00C78C28 /* BlockImageViewModel.swift in Sources */, 2AAF343C29559BF700921320 /* TreeWidgetViewModel.swift in Sources */, + 3DE3DE652BBDBAA300C9E6D3 /* MembershipPricingView.swift in Sources */, 2AFF7AC4299CDA6900B7746B /* BinSubscriptionService.swift in Sources */, 2A65E9B42AE1294900D40657 /* InitialCoordinatorViewModel.swift in Sources */, 2A8302BD2B07A058002D6655 /* AnytypeNavigationAdditionalSafeArea.swift in Sources */, @@ -11556,6 +11562,7 @@ 2A376F5A2B85F30600CF444A /* SpacesManagerRowView.swift in Sources */, 2AE906E9296FFE820044A10A /* TextIconPickerModuleAssembly.swift in Sources */, 2A85B0432AA637F100988ABD /* ApplicationCoordinatorViewModel.swift in Sources */, + 3DE3DE672BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift in Sources */, 125683D527DF17CE003A8251 /* TagSearchRowView.swift in Sources */, 3DCEE135273C18FC00BC5565 /* BeginingOfTextMarkdown.swift in Sources */, 122898BD276F112D004B81B8 /* RelationObjectsRowView.swift in Sources */, @@ -12036,7 +12043,6 @@ 2A8C00F729C4AAF200309C14 /* SettingsView.swift in Sources */, EA57F41D26B99937007D4292 /* MarkupViewModelProtocol.swift in Sources */, 2ACB798E2BAAD7B0003A2A56 /* ActiveSpaceParticipantStorage.swift in Sources */, - 2A4063632A5D480E00ECEA79 /* AsyncHelpers.swift in Sources */, C9C536AD276C9040003CD3F3 /* Color+Assets.swift in Sources */, C9A59DEF27E0F4D30063D3E1 /* TextBlockLeadingView.swift in Sources */, 2E98634F2937B55800304645 /* GroupsSubscriptionData.swift in Sources */, diff --git a/Anytype.xcodeproj/xcshareddata/xcschemes/Anytype.xcscheme b/Anytype.xcodeproj/xcshareddata/xcschemes/Anytype.xcscheme index 06674533f5..8a50c0c85f 100644 --- a/Anytype.xcodeproj/xcshareddata/xcschemes/Anytype.xcscheme +++ b/Anytype.xcodeproj/xcshareddata/xcschemes/Anytype.xcscheme @@ -76,6 +76,9 @@ isEnabled = "YES"> + + String { + return Loc.tr("Localizable", "per x days", String(describing: p1), fallback: "per %@ days") + } + /// per %@ month + internal static func perXMonths(_ p1: Any) -> String { + return Loc.tr("Localizable", "per x months", String(describing: p1), fallback: "per %@ month") + } + /// per %@ weeks + internal static func perXWeeks(_ p1: Any) -> String { + return Loc.tr("Localizable", "per x weeks", String(describing: p1), fallback: "per %@ weeks") + } /// per %@ years internal static func perXYears(_ p1: Any) -> String { return Loc.tr("Localizable", "per x years", String(describing: p1), fallback: "per %@ years") @@ -569,6 +587,8 @@ internal enum Loc { internal static let unknown = Loc.tr("Localizable", "Unknown", fallback: "Unknown") /// Unknown error internal static let unknownError = Loc.tr("Localizable", "Unknown error", fallback: "Unknown error") + /// Unlimited + internal static let unlimited = Loc.tr("Localizable", "unlimited", fallback: "Unlimited") /// Unlock internal static let unlock = Loc.tr("Localizable", "Unlock", fallback: "Unlock") /// Unpin diff --git a/Anytype/Resources/Strings/en.lproj/Localizable.strings b/Anytype/Resources/Strings/en.lproj/Localizable.strings index 7d7229e3e9..bd2009195a 100644 --- a/Anytype/Resources/Strings/en.lproj/Localizable.strings +++ b/Anytype/Resources/Strings/en.lproj/Localizable.strings @@ -1040,6 +1040,16 @@ "Learn more" = "Learn more"; "per year" = "per year"; "per x years" = "per %@ years"; +"per month" = "per month"; +"per x months" = "per %@ month"; +"per week" = "per week"; +"per x weeks" = "per %@ weeks"; +"per day" = "per day"; +"per x days" = "per %@ days"; +"per" = "per"; +"unlimited" = "Unlimited"; + + "What’s included" = "What’s included"; "Submit" = "Submit"; "Resend" = "Resend"; @@ -1053,7 +1063,6 @@ "Paid by" = "Paid by %@"; "Myself" = "Myself"; "Pay by Card" = "Pay by Card"; -"Details upon request" = "Details upon request"; "Min X characters" = "Min %@ characters"; "Pending" = "Pending..."; diff --git a/Anytype/Sources/Helpers/Extensions/StringExtensions.swift b/Anytype/Sources/Helpers/Extensions/StringExtensions.swift index 51166091e2..128678b3c6 100644 --- a/Anytype/Sources/Helpers/Extensions/StringExtensions.swift +++ b/Anytype/Sources/Helpers/Extensions/StringExtensions.swift @@ -1,4 +1,6 @@ import UIKit +import AnytypeCore + extension String { @@ -41,4 +43,21 @@ extension String { let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailTest.evaluate(with: self) } + + func dropTrailingZerosFromPrice() -> String { + let components = components(separatedBy: ".") + + if components.count == 1 { return self } + + guard components.count == 2, let major = components[safe: 0], let minor = components[safe: 1] else { + anytypeAssertionFailure("UnsupportedPriceFormat \(self)") + return self + } + + let trimmedMinor = minor.replacingOccurrences(of: "0+$", with: "", options: .regularExpression) + + if trimmedMinor.isEmpty { return major } + + return [major, trimmedMinor].joined(separator: ".") + } } diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift index 314163262d..de3d9c726b 100644 --- a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift +++ b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift @@ -17,7 +17,7 @@ struct MembershipCoordinator: View { } else { MembershipModuleView( membership: model.userMembership, - tiers: model.tiers + tiers: model.tiers ) { tier in model.onTierSelected(tier: tier) } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift new file mode 100644 index 0000000000..5f533bd363 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift @@ -0,0 +1,24 @@ +import SwiftUI +import Services + + +struct MembershipPricingView: View { + let tier: MembershipTier + + var body: some View { + switch tier.paymentType { + case .email: + AnytypeText(Loc.justEMail, style: .bodySemibold, color: .Text.primary) + case .appStore(let product): + AnytypeText("\(product.displayPrice.dropTrailingZerosFromPrice()) ", style: .bodySemibold, color: .Text.primary) + + AnytypeText(product.localizedPeriod ?? "", style: .caption1Regular, color: .Text.primary) + case .external(let info): + AnytypeText("$\(info.displayPrice.dropTrailingZerosFromPrice()) ", style: .bodySemibold, color: .Text.primary) + + AnytypeText(info.localizedPeriod ?? "", style: .caption1Regular, color: .Text.primary) + } + } +} + +#Preview { + MembershipPricingView(tier: .mockBuilder) +} diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTeirView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTeirView.swift index cb11425acf..d4167288bd 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTeirView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipTeirView.swift @@ -77,26 +77,11 @@ struct MembershipTeirView: View { AnytypeText(Loc.pending, style: .caption1Regular, color: .Text.primary) } } else { - priceText + MembershipPricingView(tier: tierToDisplay) } } } - var priceText: some View { - switch tierToDisplay.type { - case .explorer: - AnytypeText(Loc.justEMail, style: .bodySemibold, color: .Text.primary) - case .builder: - AnytypeText("$99 ", style: .bodySemibold, color: .Text.primary) + - AnytypeText(Loc.perYear, style: .caption1Regular, color: .Text.primary) - case .coCreator: - AnytypeText("$299 ", style: .bodySemibold, color: .Text.primary) + - AnytypeText(Loc.perXYears(3), style: .caption1Regular, color: .Text.primary) - case .custom: - AnytypeText(Loc.detailsUponRequest, style: .caption1Regular, color: .Text.primary) - } - } - var expirationText: some View { Group { switch tierToDisplay.type { diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift index bd6fc13b58..5936271910 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierExtensions.swift @@ -79,7 +79,8 @@ extension MembershipTier { Loc.Membership.Feature.sharedSpaces(3), Loc.Membership.Feature.spaceWriters(3), Loc.Membership.Feature.viewers(3) - ] + ], + paymentType: .email ) } @@ -93,7 +94,8 @@ extension MembershipTier { Loc.Membership.Feature.sharedSpaces(3), Loc.Membership.Feature.spaceWriters(10), Loc.Membership.Feature.viewers("Unlimited") - ] + ], + paymentType: .mockExternal ) } @@ -107,7 +109,8 @@ extension MembershipTier { Loc.Membership.Feature.sharedSpaces(3), Loc.Membership.Feature.spaceWriters(10), Loc.Membership.Feature.viewers("Unlimited") - ] + ], + paymentType: .mockExternal ) } @@ -121,7 +124,8 @@ extension MembershipTier { Loc.Membership.Feature.sharedSpaces(333), Loc.Membership.Feature.spaceWriters(100), Loc.Membership.Feature.viewers("Unlimited") - ] + ], + paymentType: .mockExternal ) } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift new file mode 100644 index 0000000000..cba6d07546 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift @@ -0,0 +1,106 @@ +import Services +import StoreKit +import AnytypeCore + + +extension MembershipTierPaymentType { + var localizedPeriod: String? { + switch self { + case .email: + anytypeAssertionFailure("No localized period for email") + return nil + case .appStore(let product): + return product.localizedPeriod + case .external(let info): + return info.localizedPeriod + } + } +} + +extension StripePaymentInfo { + var localizedPeriod: String? { + switch periodType { + case .days: + if periodValue == 1 { + return Loc.perDay + } else { + return Loc.perXDays(periodValue) + } + case .weeks: + if periodValue == 1 { + return Loc.perWeek + } else { + return Loc.perXWeeks(periodValue) + } + case .months: + if periodValue == 1 { + return Loc.perMonth + } else { + return Loc.perXMonths(periodValue) + } + case .years: + if periodValue == 1 { + return Loc.perYear + } else { + return Loc.perXYears(periodValue) + } + case .unlimited: + return Loc.unlimited + case .UNRECOGNIZED, .unknown: + anytypeAssertionFailure("Not supported period \(periodType)") + return "\(Loc.per) \(periodValue) \(periodType)" + } + } +} + +extension Product { + var localizedPeriod: String? { + guard let period = subscription?.subscriptionPeriod else { + anytypeAssertionFailure("No subscription for product \(self)") + return nil + } + + switch period.unit { + case .day: + if period.value == 1 { + return Loc.perDay + } else { + return Loc.perXDays(period.value) + } + case .week: + if period.value == 1 { + return Loc.perWeek + } else { + return Loc.perXWeeks(period.value) + } + case .month: + if period.value == 1 { + return Loc.perMonth + } else { + return Loc.perXMonths(period.value) + } + case .year: + if period.value == 1 { + return Loc.perYear + } else { + return Loc.perXYears(period.value) + } + @unknown default: + anytypeAssertionFailure("Not supported period \(period.unit)") + return "\(Loc.per) \(period.value) \(period.unit)" + } + } +} + +// MARK: - Mocks +public extension MembershipTierPaymentType { + static var mockExternal: MembershipTierPaymentType { + .external(info: .mockInfo) + } +} + +extension StripePaymentInfo { + static var mockInfo: StripePaymentInfo { + StripePaymentInfo(periodType: .years, periodValue: 1, priceInCents: 10000) + } +} diff --git a/Anytype/Supporting files/LocalStoreKitConfiguration.storekit b/Anytype/Supporting files/LocalStoreKitConfiguration.storekit new file mode 100644 index 0000000000..8f54935b5e --- /dev/null +++ b/Anytype/Supporting files/LocalStoreKitConfiguration.storekit @@ -0,0 +1,99 @@ +{ + "identifier" : "EAB7B9CD", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_failTransactionsEnabled" : false, + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "22608A4E", + "localizations" : [ + + ], + "name" : "Membership", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "100", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "A68C4FE9", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "Membership.Subscription.Builder", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "Builder", + "subscriptionGroupID" : "22608A4E", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 3, + "minor" : 0 + } +} diff --git a/Anytype/Sources/Utilities/AsyncHelpers.swift b/Modules/AnytypeCore/AnytypeCore/Utils/AsyncHelpers.swift similarity index 94% rename from Anytype/Sources/Utilities/AsyncHelpers.swift rename to Modules/AnytypeCore/AnytypeCore/Utils/AsyncHelpers.swift index 7d5ed02f24..4c963bd218 100644 --- a/Anytype/Sources/Utilities/AsyncHelpers.swift +++ b/Modules/AnytypeCore/AnytypeCore/Utils/AsyncHelpers.swift @@ -1,6 +1,6 @@ import Foundation -extension Sequence { +public extension Sequence { func asyncForEach(_ operation: (Element) async throws -> Void) async rethrows { for element in self { diff --git a/Modules/Services/Sources/Services/Membership/MembershipService.swift b/Modules/Services/Sources/Services/Membership/MembershipService.swift index d0dffce73b..b9723166e6 100644 --- a/Modules/Services/Sources/Services/Membership/MembershipService.swift +++ b/Modules/Services/Sources/Services/Membership/MembershipService.swift @@ -1,6 +1,7 @@ import ProtobufMessages import Foundation import AnytypeCore +import StoreKit public typealias MiddlewareMemberhsipStatus = Anytype_Model_Membership @@ -54,7 +55,9 @@ final class MembershipService: MembershipServiceProtocol { $0.locale = Locale.current.languageCode ?? "en" $0.noCache = noCache }) - .invoke().tiers.filter { !$0.isTest }.compactMap { $0.asModel() } + .invoke().tiers + .filter { !$0.isTest } + .asyncMap { await buildMemberhsipTier(tier: $0) }.compactMap { $0 } } func dropTiersCache() async throws { @@ -95,4 +98,52 @@ final class MembershipService: MembershipServiceProtocol { anyName: membership.requestedAnyName ) } + + private func buildMemberhsipTier(tier: Anytype_Model_MembershipTierData) async -> MembershipTier? { + guard let type = MembershipTierType(intId: tier.id) else { return nil } // ignore 0 tier + guard let paymentType = await buildMembershipPaymentType(type: type, tier: tier) else { return nil } + + let anyName: MembershipAnyName = tier.anyNamesCountIncluded > 0 ? .some(minLenght: tier.anyNameMinLength) : .none + + return MembershipTier( + type: type, + name: tier.name, + anyName: anyName, + features: tier.features, + paymentType: paymentType + ) + } + + private func buildMembershipPaymentType( + type: MembershipTierType, + tier: Anytype_Model_MembershipTierData + ) async -> MembershipTierPaymentType? { + switch type { + case .explorer: + return .email + case .builder: + let productId = "Membership.Subscription.Builder" // TODO: Get from middleware + + do { + let product = try await Product.products(for: [productId]) + guard let product = product.first else { + anytypeAssertionFailure("Not found product for id \(productId)") + return nil + } + + return .appStore(product: product) + } catch { + anytypeAssertionFailure("Get products error", info: ["error": error.localizedDescription]) + return nil + } + case .coCreator, .custom: + let info = StripePaymentInfo( + periodType: tier.periodType, + periodValue: tier.periodValue, + priceInCents: tier.priceStripeUsdCents + ) + return .external(info: info) + + } + } } diff --git a/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift b/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift index 189cf5a35b..0acc4cf611 100644 --- a/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipTier.swift @@ -51,6 +51,7 @@ public struct MembershipTier: Hashable, Identifiable, Equatable { public let name: String public let anyName: MembershipAnyName public let features: [String] + public let paymentType: MembershipTierPaymentType public var id: MembershipTierType { type } @@ -58,29 +59,13 @@ public struct MembershipTier: Hashable, Identifiable, Equatable { type: MembershipTierType, name: String, anyName: MembershipAnyName, - features: [String] + features: [String], + paymentType: MembershipTierPaymentType ) { self.type = type self.name = name self.anyName = anyName self.features = features - } -} - - -// MARK: - Middleware model mapping - -extension Anytype_Model_MembershipTierData { - func asModel() -> MembershipTier? { - guard let type = MembershipTierType(intId: id) else { return nil } - - let anyName: MembershipAnyName = anyNamesCountIncluded > 0 ? .some(minLenght: anyNameMinLength) : .none - - return MembershipTier( - type: type, - name: name, - anyName: anyName, - features: features - ) + self.paymentType = paymentType } } diff --git a/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift b/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift new file mode 100644 index 0000000000..f9b44d8b2c --- /dev/null +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift @@ -0,0 +1,27 @@ +import StoreKit +import ProtobufMessages + + +public typealias MemberhipTierPaymentPeriodType = Anytype_Model_MembershipTierData.PeriodType + +public struct StripePaymentInfo: Hashable, Equatable { + public let periodType: MemberhipTierPaymentPeriodType + public let periodValue: UInt32 + public let priceInCents: UInt32 + + public var displayPrice: String { + return "\(priceInCents/100).\(priceInCents%100)" + } + + public init(periodType: MemberhipTierPaymentPeriodType, periodValue: UInt32, priceInCents: UInt32) { + self.periodType = periodType + self.periodValue = periodValue + self.priceInCents = priceInCents + } +} + +public enum MembershipTierPaymentType: Hashable, Equatable { + case email + case appStore(product: Product) + case external(info: StripePaymentInfo) +} From 57e496688741ce1d8586e679ead0f185a5a9b7e4 Mon Sep 17 00:00:00 2001 From: Vova Ignatov Date: Thu, 4 Apr 2024 09:52:15 +0100 Subject: [PATCH 2/4] IOS-2594 Add plural localization --- Anytype/Generated/Strings.swift | 40 +++++------- .../Strings/en.lproj/Localizable.strings | 8 --- .../Strings/en.lproj/Localizable.stringsdict | 64 +++++++++++++++++++ .../MembershipTierPaymentTypeExtension.swift | 48 +++----------- .../NameSheet/MembershipNameSheetView.swift | 2 +- 5 files changed, 89 insertions(+), 73 deletions(-) diff --git a/Anytype/Generated/Strings.swift b/Anytype/Generated/Strings.swift index 0a6dc95f2f..c8a6a67b23 100644 --- a/Anytype/Generated/Strings.swift +++ b/Anytype/Generated/Strings.swift @@ -398,30 +398,22 @@ internal enum Loc { internal static let pendingDeletionText = Loc.tr("Localizable", "Pending deletion text", fallback: "We're sorry to see you go. You have 30 days to cancel this request. After 30 days, your encrypted account data is permanently removed from the backup node.") /// per internal static let per = Loc.tr("Localizable", "per", fallback: "per") - /// per day - internal static let perDay = Loc.tr("Localizable", "per day", fallback: "per day") - /// per month - internal static let perMonth = Loc.tr("Localizable", "per month", fallback: "per month") - /// per week - internal static let perWeek = Loc.tr("Localizable", "per week", fallback: "per week") - /// per %@ days - internal static func perXDays(_ p1: Any) -> String { - return Loc.tr("Localizable", "per x days", String(describing: p1), fallback: "per %@ days") - } - /// per %@ month - internal static func perXMonths(_ p1: Any) -> String { - return Loc.tr("Localizable", "per x months", String(describing: p1), fallback: "per %@ month") - } - /// per %@ weeks - internal static func perXWeeks(_ p1: Any) -> String { - return Loc.tr("Localizable", "per x weeks", String(describing: p1), fallback: "per %@ weeks") - } - /// per %@ years - internal static func perXYears(_ p1: Any) -> String { - return Loc.tr("Localizable", "per x years", String(describing: p1), fallback: "per %@ years") - } - /// per year - internal static let perYear = Loc.tr("Localizable", "per year", fallback: "per year") + /// Plural format key: "per %#@day@" + internal static func perDay(_ p1: Int) -> String { + return Loc.tr("Localizable", "Per Day", p1, fallback: "Plural format key: \"per %#@day@\"") + } + /// Plural format key: "per %#@month@" + internal static func perMonth(_ p1: Int) -> String { + return Loc.tr("Localizable", "Per Month", p1, fallback: "Plural format key: \"per %#@month@\"") + } + /// Plural format key: "per %#@week@" + internal static func perWeek(_ p1: Int) -> String { + return Loc.tr("Localizable", "Per Week", p1, fallback: "Plural format key: \"per %#@week@\"") + } + /// Plural format key: "per %#@year@" + internal static func perYear(_ p1: Int) -> String { + return Loc.tr("Localizable", "Per Year", p1, fallback: "Plural format key: \"per %#@year@\"") + } /// Personalization internal static let personalization = Loc.tr("Localizable", "Personalization", fallback: "Personalization") /// Picture diff --git a/Anytype/Resources/Strings/en.lproj/Localizable.strings b/Anytype/Resources/Strings/en.lproj/Localizable.strings index bd2009195a..64c8f0fd51 100644 --- a/Anytype/Resources/Strings/en.lproj/Localizable.strings +++ b/Anytype/Resources/Strings/en.lproj/Localizable.strings @@ -1038,14 +1038,6 @@ "Just e-mail" = "Just e-mail"; "E-mail" = "E-mail"; "Learn more" = "Learn more"; -"per year" = "per year"; -"per x years" = "per %@ years"; -"per month" = "per month"; -"per x months" = "per %@ month"; -"per week" = "per week"; -"per x weeks" = "per %@ weeks"; -"per day" = "per day"; -"per x days" = "per %@ days"; "per" = "per"; "unlimited" = "Unlimited"; diff --git a/Anytype/Resources/Strings/en.lproj/Localizable.stringsdict b/Anytype/Resources/Strings/en.lproj/Localizable.stringsdict index 81d2b44f8b..ab6d48d365 100644 --- a/Anytype/Resources/Strings/en.lproj/Localizable.stringsdict +++ b/Anytype/Resources/Strings/en.lproj/Localizable.stringsdict @@ -212,5 +212,69 @@ %d request + Per Month + + NSStringLocalizedFormatKey + per %#@month@ + month + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + month + other + %d months + + + Per Week + + NSStringLocalizedFormatKey + per %#@week@ + week + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + week + other + %d weeks + + + Per Year + + NSStringLocalizedFormatKey + per %#@year@ + year + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + year + other + %d years + + + Per Day + + NSStringLocalizedFormatKey + per %#@day@ + day + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + day + other + %d days + + diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift index cba6d07546..0a5de6488c 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift @@ -21,29 +21,13 @@ extension StripePaymentInfo { var localizedPeriod: String? { switch periodType { case .days: - if periodValue == 1 { - return Loc.perDay - } else { - return Loc.perXDays(periodValue) - } + return Loc.perDay(Int(periodValue)) case .weeks: - if periodValue == 1 { - return Loc.perWeek - } else { - return Loc.perXWeeks(periodValue) - } + return Loc.perWeek(Int(periodValue)) case .months: - if periodValue == 1 { - return Loc.perMonth - } else { - return Loc.perXMonths(periodValue) - } + return Loc.perMonth(Int(periodValue)) case .years: - if periodValue == 1 { - return Loc.perYear - } else { - return Loc.perXYears(periodValue) - } + return Loc.perYear(Int(periodValue)) case .unlimited: return Loc.unlimited case .UNRECOGNIZED, .unknown: @@ -62,29 +46,13 @@ extension Product { switch period.unit { case .day: - if period.value == 1 { - return Loc.perDay - } else { - return Loc.perXDays(period.value) - } + return Loc.perDay(period.value) case .week: - if period.value == 1 { - return Loc.perWeek - } else { - return Loc.perXWeeks(period.value) - } + return Loc.perWeek(period.value) case .month: - if period.value == 1 { - return Loc.perMonth - } else { - return Loc.perXMonths(period.value) - } + return Loc.perMonth(period.value) case .year: - if period.value == 1 { - return Loc.perYear - } else { - return Loc.perXYears(period.value) - } + return Loc.perYear(period.value) @unknown default: anytypeAssertionFailure("Not supported period \(period.unit)") return "\(Loc.per) \(period.value) \(period.unit)" diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift index c2ac592814..83591e208b 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift @@ -28,7 +28,7 @@ struct MembershipNameSheetView: View { .newDivider() status AnytypeText("$99 ", style: .title, color: .Text.primary) + - AnytypeText(Loc.perYear, style: .relation1Regular, color: .Text.primary) + AnytypeText(Loc.perYear(1), style: .relation1Regular, color: .Text.primary) Spacer.fixedHeight(15) StandardButton( Loc.payByCard, From 5361e14db0465ccacff482b57cb678af4f5681d7 Mon Sep 17 00:00:00 2001 From: Vova Ignatov Date: Thu, 4 Apr 2024 15:46:51 +0100 Subject: [PATCH 3/4] IOS-2594 Use formatted display price --- .../Helpers/Extensions/StringExtensions.swift | 17 ----------------- .../Views/MembershipPricingView.swift | 6 +++--- .../MembershipTierPaymentTypeExtension.swift | 11 +++++++++++ .../Model/MembershipTierPaymentType.swift | 4 ---- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Anytype/Sources/Helpers/Extensions/StringExtensions.swift b/Anytype/Sources/Helpers/Extensions/StringExtensions.swift index 128678b3c6..d9211f167b 100644 --- a/Anytype/Sources/Helpers/Extensions/StringExtensions.swift +++ b/Anytype/Sources/Helpers/Extensions/StringExtensions.swift @@ -43,21 +43,4 @@ extension String { let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailTest.evaluate(with: self) } - - func dropTrailingZerosFromPrice() -> String { - let components = components(separatedBy: ".") - - if components.count == 1 { return self } - - guard components.count == 2, let major = components[safe: 0], let minor = components[safe: 1] else { - anytypeAssertionFailure("UnsupportedPriceFormat \(self)") - return self - } - - let trimmedMinor = minor.replacingOccurrences(of: "0+$", with: "", options: .regularExpression) - - if trimmedMinor.isEmpty { return major } - - return [major, trimmedMinor].joined(separator: ".") - } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift index 5f533bd363..4874c9b348 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/InitialScreen/Views/MembershipPricingView.swift @@ -10,10 +10,10 @@ struct MembershipPricingView: View { case .email: AnytypeText(Loc.justEMail, style: .bodySemibold, color: .Text.primary) case .appStore(let product): - AnytypeText("\(product.displayPrice.dropTrailingZerosFromPrice()) ", style: .bodySemibold, color: .Text.primary) + + AnytypeText("\(product.anytypeDisplayPrice) ", style: .bodySemibold, color: .Text.primary) + AnytypeText(product.localizedPeriod ?? "", style: .caption1Regular, color: .Text.primary) - case .external(let info): - AnytypeText("$\(info.displayPrice.dropTrailingZerosFromPrice()) ", style: .bodySemibold, color: .Text.primary) + + case .external(let info): + AnytypeText("\(info.displayPrice) ", style: .bodySemibold, color: .Text.primary) + AnytypeText(info.localizedPeriod ?? "", style: .caption1Regular, color: .Text.primary) } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift index 0a5de6488c..d6e07fe859 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift @@ -35,6 +35,13 @@ extension StripePaymentInfo { return "\(Loc.per) \(periodValue) \(periodType)" } } + + public var displayPrice: String { + Decimal(Double(priceInCents)/100) + .formatted(.currency(code: "USD") + .precision(.fractionLength(0...2)) + ) + } } extension Product { @@ -58,6 +65,10 @@ extension Product { return "\(Loc.per) \(period.value) \(period.unit)" } } + + var anytypeDisplayPrice: String { + price.formatted(priceFormatStyle.precision(.fractionLength(0...2))) + } } // MARK: - Mocks diff --git a/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift b/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift index f9b44d8b2c..5db9dfd693 100644 --- a/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift @@ -9,10 +9,6 @@ public struct StripePaymentInfo: Hashable, Equatable { public let periodValue: UInt32 public let priceInCents: UInt32 - public var displayPrice: String { - return "\(priceInCents/100).\(priceInCents%100)" - } - public init(periodType: MemberhipTierPaymentPeriodType, periodValue: UInt32, priceInCents: UInt32) { self.periodType = periodType self.periodValue = periodValue From 33cb17e7a783979443a09ade712ca6b1095ad725 Mon Sep 17 00:00:00 2001 From: Vova Ignatov Date: Thu, 4 Apr 2024 15:58:51 +0100 Subject: [PATCH 4/4] IOS-2594 Use dynamic price in payment screen --- .../Models/MembershipTierPaymentTypeExtension.swift | 12 ++++++++++++ .../NameSheet/MembershipNameSheetView.swift | 4 ++-- .../NameSheet/MembershipNameSheetViewModel.swift | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift index d6e07fe859..cb2f0f00fc 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift @@ -15,6 +15,18 @@ extension MembershipTierPaymentType { return info.localizedPeriod } } + + var displayPrice: String? { + switch self { + case .email: + anytypeAssertionFailure("No display price for email") + return nil + case .appStore(let product): + return product.anytypeDisplayPrice + case .external(let info): + return info.displayPrice + } + } } extension StripePaymentInfo { diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift index 83591e208b..6aa0b97a0e 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift @@ -27,8 +27,8 @@ struct MembershipNameSheetView: View { nameInput .newDivider() status - AnytypeText("$99 ", style: .title, color: .Text.primary) + - AnytypeText(Loc.perYear(1), style: .relation1Regular, color: .Text.primary) + AnytypeText("\(model.tier.paymentType.displayPrice ?? "") ", style: .title, color: .Text.primary) + + AnytypeText(model.tier.paymentType.localizedPeriod ?? "", style: .relation1Regular, color: .Text.primary) Spacer.fixedHeight(15) StandardButton( Loc.payByCard, diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift index fdb76cb1ab..caf7f858b9 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift @@ -23,6 +23,7 @@ enum MembershipNameSheetViewState { final class MembershipNameSheetViewModel: ObservableObject { @Published var state = MembershipNameSheetViewState.default let anyName: String + let tier: MembershipTier var minimumNumberOfCharacters: UInt32 { switch tier.anyName { @@ -37,7 +38,6 @@ final class MembershipNameSheetViewModel: ObservableObject { @Injected(\.membershipService) private var memberhsipService: MembershipServiceProtocol - private let tier: MembershipTier private var validationTask: Task<(), any Error>? init(tier: MembershipTier, anyName: String) {