diff --git a/Anytype.xcodeproj/project.pbxproj b/Anytype.xcodeproj/project.pbxproj index ec266a1963..389347b074 100644 --- a/Anytype.xcodeproj/project.pbxproj +++ b/Anytype.xcodeproj/project.pbxproj @@ -414,7 +414,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 */; }; @@ -1381,6 +1380,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 */; }; @@ -2208,7 +2209,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 = ""; }; @@ -3170,6 +3170,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 = ""; }; @@ -3753,6 +3756,7 @@ 038FB5C522D8F2A0002B90B8 /* Supporting files */ = { isa = PBXGroup; children = ( + 3DE3DE632BBD7E6F00C9E6D3 /* LocalStoreKitConfiguration.storekit */, 0303ED0922D8EDAD005C552B /* LaunchScreen.storyboard */, 0303ED0C22D8EDAD005C552B /* Info.plist */, ); @@ -4180,7 +4184,6 @@ 1270CE8E26DA848B00B2D443 /* Images */, EAC5FD4A2657DC3D006BB24E /* UIScrollView */, 0A996BD023F0342A000FDC9C /* PlistReader.swift */, - 2A4063622A5D480E00ECEA79 /* AsyncHelpers.swift */, 2AEDACC32B9F3A4600FA757A /* AppContext.swift */, ); path = Utilities; @@ -8735,6 +8738,7 @@ children = ( 3D611E952B93779900B33305 /* MembershipTierExtensions.swift */, 3DFD50A32BA0B114002E51F7 /* MembershipStatusExtension.swift */, + 3DE3DE662BBDBC1F00C9E6D3 /* MembershipTierPaymentTypeExtension.swift */, ); path = Models; sourceTree = ""; @@ -9228,6 +9232,7 @@ 3DD0CEBA2B90E6BB00657402 /* MembershipBannerView.swift */, 3D611E8D2B936B8300B33305 /* MembershipTeirView.swift */, 3D611E922B93775B00B33305 /* MembershipTierListView.swift */, + 3DE3DE642BBDBAA300C9E6D3 /* MembershipPricingView.swift */, ); path = Views; sourceTree = ""; @@ -10847,6 +10852,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 */, @@ -11568,6 +11574,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 */, @@ -12047,7 +12054,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 years", String(describing: p1), fallback: "per %@ years") + /// per + internal static let per = Loc.tr("Localizable", "per", fallback: "per") + /// 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@\"") } - /// per year - internal static let perYear = Loc.tr("Localizable", "per year", fallback: "per year") /// Personalization internal static let personalization = Loc.tr("Localizable", "Personalization", fallback: "Personalization") /// Picture @@ -569,6 +579,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..64c8f0fd51 100644 --- a/Anytype/Resources/Strings/en.lproj/Localizable.strings +++ b/Anytype/Resources/Strings/en.lproj/Localizable.strings @@ -1038,8 +1038,10 @@ "Just e-mail" = "Just e-mail"; "E-mail" = "E-mail"; "Learn more" = "Learn more"; -"per year" = "per year"; -"per x years" = "per %@ years"; +"per" = "per"; +"unlimited" = "Unlimited"; + + "What’s included" = "What’s included"; "Submit" = "Submit"; "Resend" = "Resend"; @@ -1053,7 +1055,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/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/Helpers/Extensions/StringExtensions.swift b/Anytype/Sources/Helpers/Extensions/StringExtensions.swift index 51166091e2..d9211f167b 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 { diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift index 59bf579d26..ff29d3c420 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..4874c9b348 --- /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.anytypeDisplayPrice) ", style: .bodySemibold, color: .Text.primary) + + AnytypeText(product.localizedPeriod ?? "", style: .caption1Regular, color: .Text.primary) + case .external(let info): + AnytypeText("\(info.displayPrice) ", 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..cb2f0f00fc --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/Models/MembershipTierPaymentTypeExtension.swift @@ -0,0 +1,97 @@ +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 + } + } + + 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 { + var localizedPeriod: String? { + switch periodType { + case .days: + return Loc.perDay(Int(periodValue)) + case .weeks: + return Loc.perWeek(Int(periodValue)) + case .months: + return Loc.perMonth(Int(periodValue)) + case .years: + return Loc.perYear(Int(periodValue)) + case .unlimited: + return Loc.unlimited + case .UNRECOGNIZED, .unknown: + anytypeAssertionFailure("Not supported period \(periodType)") + return "\(Loc.per) \(periodValue) \(periodType)" + } + } + + public var displayPrice: String { + Decimal(Double(priceInCents)/100) + .formatted(.currency(code: "USD") + .precision(.fractionLength(0...2)) + ) + } +} + +extension Product { + var localizedPeriod: String? { + guard let period = subscription?.subscriptionPeriod else { + anytypeAssertionFailure("No subscription for product \(self)") + return nil + } + + switch period.unit { + case .day: + return Loc.perDay(period.value) + case .week: + return Loc.perWeek(period.value) + case .month: + return Loc.perMonth(period.value) + case .year: + return Loc.perYear(period.value) + @unknown default: + anytypeAssertionFailure("Not supported period \(period.unit)") + return "\(Loc.per) \(period.value) \(period.unit)" + } + } + + var anytypeDisplayPrice: String { + price.formatted(priceFormatStyle.precision(.fractionLength(0...2))) + } +} + +// 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/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift index c2ac592814..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, 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) { 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..5db9dfd693 --- /dev/null +++ b/Modules/Services/Sources/Services/Membership/Model/MembershipTierPaymentType.swift @@ -0,0 +1,23 @@ +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 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) +}